Files
qlib/tests/ops/test_expression_compatibility.py
5435523246 fix(security): constrain config-driven code execution (#2340)
* security: constrain config-driven code execution

* fix: address security test and lint failures

* ci: restore dependency and title lint compatibility

* ci: handle current MLflow and Windows OSQP changes

* ci: use lxml release with cross-platform wheels

* fix: defer optional CVXPY solver imports

* style: document intentional lazy import

* ci: skip broken released OSQP on Windows Python 3.8

* test: make MLflow client timing check robust

* ci: retry nondeterministic benchmark smoke test

* fix: cap filelock before fork safety regression

* fix: use filelock release compatible with multiprocessing

* fix(security): restrict operator lookup and validate trusted roots

* fix(security): preserve numeric expressions and legacy model imports

* fix: preserve TRA initialization and document config migration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: preserve safe expression syntax and cache semantics

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: use explicit scoped trust for file modules

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: satisfy CI lint without weakening trust checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: handle escaped file paths in module refusal errors

Match the repr-formatted module path in refusal messages and cover Windows, UNC, quoted, and newline-containing paths on every platform.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: preserve expression parser error logging

Log ExpressionSyntaxError with its field and traceback before re-raising, while preserving custom operator NameError and SyntaxError handling. Add five regression cases covering logging, exception identity, and failed cache entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: isolate expression logger capture

Capture parser logs directly on qlib.data without propagating to parent and root capture handlers. Restore logger state with monkeypatch and retain exact single-record, traceback, and exception-identity assertions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(security): preserve malformed feature boundaries and reject unsupported literals

* test(security): cover malformed dollar-prefixed expressions

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Yang Liu <DG-git-dev@users.noreply.github.com>
2026-10-08 18:15:27 +08:00

56 lines
2.5 KiB
Python

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
import pandas as pd
from qlib.data import D
from qlib.data.ops import Ref
from qlib.tests import TestAutoData
class MetadataRef(Ref):
def __init__(self, feature, N=1, metadata=None):
super().__init__(feature, N)
assert metadata == {"label": "close price", "literal": "$close", "window": (1, None)}
class TestExpressionCompatibility(TestAutoData):
@classmethod
def setUpClass(cls):
cls._setup_kwargs = dict(cls._setup_kwargs, kernels=1, custom_ops=[MetadataRef])
super().setUpClass()
def test_real_data_queries_preserve_values(self):
cases = [
("Ref(*[$close, 1])", "Ref($close, 1)"),
("Ref(*($close, 1))", "Ref($close, 1)"),
("Ref(*[$close, *[1]])", "Ref($close, 1)"),
("Ref($close, **{'N': 1})", "Ref($close, 1)"),
("Ref(**{'feature': $close, 'N': 1})", "Ref($close, 1)"),
("Ref($close, **{'N': 2, **{'N': 1}})", "Ref($close, 1)"),
("Ref($close, [1, 5][0])", "Ref($close, 1)"),
("Ref($close, {'window': 1}['window'])", "Ref($close, 1)"),
("Ref(*[$close, 1, 99][:2])", "Ref($close, 1)"),
("Ref(*[1, $close][::-1])", "Ref($close, 1)"),
("Ref($close, 1 if (2 > 1 and not False) else 2)", "Ref($close, 1)"),
("Ref($close, 0 or (1))", "Ref($close, 1)"),
("Ref($close, 1 if 0 < 1 < 2 else 2)", "Ref($close, 1)"),
("Ref($close, 1 if True else 1 / 0)", "Ref($close, 1)"),
("Mean($close, **{'N': 2 + 3})", "Mean($close, 5)"),
("$close if True else $open", "$close"),
("If($close > $open, Ref($close, 1 if True else 2), $open)", "If($close > $open, Ref($close, 1), $open)"),
("Feature('close'[::1])", "$close"),
(
"MetadataRef(*[$close], **{'N': 1, 'metadata': "
"{'label': 'close price', 'literal': '$close', 'window': (1, None)}})",
"Ref($close, 1)",
),
]
instruments = ["SH600000", "SH600519", "SZ000001"]
actual = D.features(instruments, [source for source, _ in cases], "2018-01-02", "2018-01-31")
expected = D.features(instruments, [plain for _, plain in cases], "2018-01-02", "2018-01-31")
self.assertGreater(len(actual), 0)
self.assertTrue(actual.notna().any().all())
actual.columns = expected.columns = range(len(cases))
pd.testing.assert_frame_equal(actual, expected, check_exact=True)