diff --git a/.github/workflows/stubs.yml b/.github/workflows/stubs.yml index 559f8d97b..f662c0bf3 100644 --- a/.github/workflows/stubs.yml +++ b/.github/workflows/stubs.yml @@ -20,11 +20,19 @@ jobs: stubtest: if: (github.event_name != 'pull_request') || (github.event.pull_request.draft == false) runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write env: PYTHON_VERSION: "3.14" steps: - uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref || github.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 - name: Install dependencies (SCIPOptSuite) run: | @@ -45,19 +53,119 @@ jobs: export CFLAGS="-O0 -ggdb -Wall -Wextra -Werror -Wno-error=deprecated-declarations" # Debug mode. More warnings. Warnings as errors, but allow deprecated declarations. python -m pip install . -v 2>&1 | tee build.log - - name: Run MyPy + - name: Run MyPy (before regeneration) + id: mypy_before + run: python -m mypy --package pyscipopt + continue-on-error: true + + - name: Run stubtest (before regeneration) + id: stubtest_before + run: stubs/test.sh + continue-on-error: true + + - name: Check if safe to run generate_stubs.py + id: check_script + run: | + # For same-repo PRs, always allow running the script + if [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]] || [[ "${{ github.event_name }}" != "pull_request" ]]; then + echo "allowed=true" >> $GITHUB_OUTPUT + echo "Same-repo PR or push - allowing regeneration" + else + # For fork PRs, only allow if script is unchanged from master + git fetch origin master + if git diff --quiet origin/master -- scripts/generate_stubs.py; then + echo "allowed=true" >> $GITHUB_OUTPUT + echo "Fork PR with unchanged script - allowing regeneration" + else + echo "allowed=false" >> $GITHUB_OUTPUT + echo "::warning::Fork PR with modified scripts/generate_stubs.py - skipping auto-regeneration for security" + fi + fi + + - name: Regenerate stubs + if: steps.stubtest_before.outcome == 'failure' && steps.check_script.outputs.allowed == 'true' + run: | + python scripts/generate_stubs.py + # Auto-fix lint issues and format + pip install ruff + ruff check src/pyscipopt/scip.pyi --extend-select ANN,I,PYI,RUF100 --fix + ruff format src/pyscipopt/scip.pyi + # Copy regenerated stub to installed package location for stubtest + cp src/pyscipopt/scip.pyi "$(python -c 'import pyscipopt; print(pyscipopt.__path__[0])')/scip.pyi" + + - name: Run MyPy (after regeneration) + if: steps.stubtest_before.outcome == 'failure' && steps.check_script.outputs.allowed == 'true' run: python -m mypy --package pyscipopt - - name: Run stubtest + - name: Run stubtest (after regeneration) + if: steps.stubtest_before.outcome == 'failure' && steps.check_script.outputs.allowed == 'true' run: stubs/test.sh + - name: Commit and push updated stubs + id: commit + if: steps.stubtest_before.outcome == 'failure' && github.event_name == 'pull_request' && steps.check_script.outputs.allowed == 'true' && github.event.pull_request.head.repo.full_name == github.repository + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add src/pyscipopt/scip.pyi + + # Capture the diff for the PR comment + DIFF=$(git diff --cached --stat) + echo "diff<> $GITHUB_OUTPUT + echo "$DIFF" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Capture detailed changes (use || true to handle SIGPIPE from head) + DETAILED_DIFF=$(git diff --cached src/pyscipopt/scip.pyi | head -100 || true) + echo "detailed_diff<> $GITHUB_OUTPUT + echo "$DETAILED_DIFF" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + if git diff --cached --quiet; then + echo "committed=false" >> $GITHUB_OUTPUT + else + git commit -m "Auto-regenerate type stubs" + git push + echo "committed=true" >> $GITHUB_OUTPUT + fi + + - name: Comment on PR + if: steps.commit.outputs.committed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr comment ${{ github.event.pull_request.number }} --body "## 🤖 Type stubs automatically regenerated + + The type stubs were out of date and have been automatically regenerated. A new commit has been pushed to this PR. + +
+ Changes summary + + \`\`\` + ${{ steps.commit.outputs.diff }} + \`\`\` + +
+ +
+ Detailed diff (first 100 lines) + + \`\`\`diff + ${{ steps.commit.outputs.detailed_diff }} + \`\`\` + +
" + lint: + needs: stubtest runs-on: ubuntu-latest env: FILES: src/pyscipopt/scip.pyi steps: - uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref || github.ref }} - name: Install Ruff uses: astral-sh/ruff-action@v3 @@ -67,5 +175,5 @@ jobs: - name: Lint type stubs run: ruff check ${{ env.FILES }} --extend-select ANN,I,PYI,RUF100 - - name: Format type stubs - run: ruff format ${{ env.FILES }} + - name: Format check type stubs + run: ruff format --check ${{ env.FILES }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 52df46228..eea4c389e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Added automated script for generating type stubs - Include parameter names in type stubs - Speed up MatrixExpr.sum(axis=...) via quicksum +- Run automated type stub generating script to PR pipeline +- Wrapped isObjIntegral() and test ### Fixed - all fundamental callbacks now raise an error if not implemented - Fixed the type of MatrixExpr.sum(axis=...) result from MatrixVariable to MatrixExpr. diff --git a/scripts/generate_stubs.py b/scripts/generate_stubs.py index 52acbaed2..9ee0fc5a2 100755 --- a/scripts/generate_stubs.py +++ b/scripts/generate_stubs.py @@ -25,13 +25,14 @@ class ClassInfo: name: str parent: Optional[str] = None attributes: list = field(default_factory=list) # list of (name, type_hint) - methods: list = field(default_factory=list) # list of method names + methods: dict = field(default_factory=dict) # dict of method_name -> list of param names (excluding self) static_methods: set = field(default_factory=set) # set of static method names class_vars: list = field(default_factory=list) # list of (name, type_hint) has_hash: bool = False has_eq: bool = False is_dataclass: bool = False dataclass_fields: list = field(default_factory=list) # list of (name, type, default) + is_statistics_class: bool = False # Special handling for Statistics class @dataclass @@ -59,11 +60,11 @@ class StubGenerator: '__hash__': 'def __hash__(self) -> int: ...', '__len__': 'def __len__(self) -> int: ...', '__bool__': 'def __bool__(self) -> bool: ...', - '__init__': 'def __init__(self, *args, **kwargs) -> None: ...', + '__init__': 'def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ...', '__repr__': 'def __repr__(self) -> str: ...', '__str__': 'def __str__(self) -> str: ...', - '__delitem__': 'def __delitem__(self, other) -> None: ...', - '__setitem__': 'def __setitem__(self, index, object) -> None: ...', + '__delitem__': 'def __delitem__(self, key: Incomplete) -> None: ...', + '__setitem__': 'def __setitem__(self, key: Incomplete, value: Incomplete) -> None: ...', } # Methods that should NOT appear in stubs (internal Cython methods) @@ -79,24 +80,49 @@ class StubGenerator: # Methods with specific type hints (no *args, **kwargs) TYPED_METHODS = { - '__getitem__': 'def __getitem__(self, index): ...', - '__iter__': 'def __iter__(self): ...', - '__next__': 'def __next__(self): ...', - '__add__': 'def __add__(self, other): ...', - '__radd__': 'def __radd__(self, other): ...', - '__sub__': 'def __sub__(self, other): ...', - '__rsub__': 'def __rsub__(self, other): ...', - '__mul__': 'def __mul__(self, other): ...', - '__rmul__': 'def __rmul__(self, other): ...', - '__truediv__': 'def __truediv__(self, other): ...', - '__rtruediv__': 'def __rtruediv__(self, other): ...', - '__pow__': 'def __pow__(self, other): ...', - '__rpow__': 'def __rpow__(self, other): ...', - '__neg__': 'def __neg__(self): ...', - '__abs__': 'def __abs__(self): ...', - '__iadd__': 'def __iadd__(self, other): ...', - '__isub__': 'def __isub__(self, other): ...', - '__imul__': 'def __imul__(self, other): ...', + '__getitem__': 'def __getitem__(self, index: Incomplete) -> Incomplete: ...', + '__iter__': 'def __iter__(self) -> Incomplete: ...', + '__next__': 'def __next__(self) -> Incomplete: ...', + '__add__': 'def __add__(self, other: Incomplete) -> Incomplete: ...', + '__radd__': 'def __radd__(self, other: Incomplete) -> Incomplete: ...', + '__sub__': 'def __sub__(self, other: Incomplete) -> Incomplete: ...', + '__rsub__': 'def __rsub__(self, other: Incomplete) -> Incomplete: ...', + '__mul__': 'def __mul__(self, other: Incomplete) -> Incomplete: ...', + '__rmul__': 'def __rmul__(self, other: Incomplete) -> Incomplete: ...', + '__truediv__': 'def __truediv__(self, other: Incomplete) -> Incomplete: ...', + '__rtruediv__': 'def __rtruediv__(self, other: Incomplete) -> Incomplete: ...', + '__rpow__': 'def __rpow__(self, other: Incomplete) -> Incomplete: ...', + '__neg__': 'def __neg__(self) -> Incomplete: ...', + '__abs__': 'def __abs__(self) -> Incomplete: ...', + '__iadd__': 'def __iadd__(self, other: Incomplete) -> Incomplete: ... # noqa: PYI034', + '__isub__': 'def __isub__(self, other: Incomplete) -> Incomplete: ... # noqa: PYI034', + '__imul__': 'def __imul__(self, other: Incomplete) -> Incomplete: ... # noqa: PYI034', + '__matmul__': 'def __matmul__(self, other: Incomplete) -> Incomplete: ...', + } + + # Classes that should have @disjoint_base decorator + # These are user-facing classes that should not be structurally compatible + DISJOINT_BASE_CLASSES = { + 'Benders', 'Benderscut', 'BoundChange', 'Branchrule', 'Column', 'ColumnExact', + 'Conshdlr', 'Constant', 'Constraint', 'Cutsel', 'DomainChanges', 'Event', + 'Eventhdlr', 'Expr', 'ExprCons', 'GenExpr', 'Heur', 'IIS', 'IISfinder', + 'LP', 'Model', 'NLRow', 'Node', 'Nodesel', 'PowExpr', 'Presol', 'Pricer', + 'ProdExpr', 'Prop', 'Reader', 'Relax', 'Row', 'RowExact', 'Sepa', 'Solution', + 'SumExpr', 'VarExpr', 'Variable', '_VarArray', + } + + # Methods that need type: ignore[override] for numpy subclasses + NUMPY_OVERRIDE_METHODS = {'__ge__', '__le__', '__gt__', '__lt__', 'sum', '__pow__'} + + # Cython extension types (cdef class) that need *args, **kwargs for __init__ + # These specific classes have __init__ with *args, **kwargs at runtime + CYTHON_EXTENSION_TYPES = { + 'PowExpr', 'ProdExpr', 'SumExpr', 'UnaryExpr', 'VarExpr', 'Constant', + } + + # Classes with custom __init__ signatures + CUSTOM_INIT = { + 'Term': 'def __init__(self, *vartuple: Incomplete) -> None: ...', } def __init__(self, src_dir: Path): @@ -405,6 +431,17 @@ def get_current_class(): i += 1 continue + # Extract full signature (may span multiple lines) + sig_lines = [stripped] + j = i + 1 + while j < len(lines) and ')' not in sig_lines[-1]: + sig_lines.append(lines[j].strip()) + j += 1 + full_sig = ' '.join(sig_lines) + + # Parse parameters from signature + params = self._parse_params(full_sig) + if current_class: cls_info = self.module_info.classes.get(current_class) if cls_info: @@ -416,9 +453,10 @@ def get_current_class(): # Expand __richcmp__ to individual comparison methods for cmp_method in self.RICHCMP_EXPANSION['__richcmp__']: if cmp_method not in cls_info.methods: - cls_info.methods.append(cmp_method) - elif method_name not in cls_info.methods: - cls_info.methods.append(method_name) + cls_info.methods[cmp_method] = [] + else: + # Always overwrite - last definition wins (like Python) + cls_info.methods[method_name] = params # Track static methods if is_staticmethod: @@ -448,6 +486,121 @@ def get_current_class(): i += 1 + def _generate_method_stub(self, method_name: str, params: list, is_static: bool = False, return_type: str = 'Incomplete') -> str: + """Generate a method stub with proper parameter types. + + Args: + method_name: Name of the method + params: List of (param_name, has_default) tuples + is_static: Whether this is a static method + return_type: Return type annotation + """ + if not params: + # No parameters (other than self) + if is_static: + return f'def {method_name}() -> {return_type}: ...' + else: + return f'def {method_name}(self) -> {return_type}: ...' + + # Special case: __pow__ third parameter should have a default (Python protocol) + if method_name == '__pow__' and len(params) >= 2: + params = list(params) # Make a copy + # Make the third parameter (modulo/mod) have a default + if len(params) >= 2 and not params[1][1]: # If second param doesn't have default + params[1] = (params[1][0], True) # Give it a default + + # Build parameter string + param_strs = [] + if not is_static: + param_strs.append('self') + + for param_name, has_default in params: + if has_default: + param_strs.append(f'{param_name}: Incomplete = ...') + else: + param_strs.append(f'{param_name}: Incomplete') + + params_str = ', '.join(param_strs) + return f'def {method_name}({params_str}) -> {return_type}: ...' + + def _parse_params(self, signature: str) -> list: + """Parse parameter names from a function signature. + + Returns a list of (param_name, has_default) tuples, excluding 'self'. + """ + # Extract the part between parentheses + match = re.search(r'\(([^)]*)\)', signature) + if not match: + return [] + + params_str = match.group(1) + if not params_str.strip(): + return [] + + params = [] + # Split by comma, but handle nested structures + depth = 0 + current = [] + for char in params_str: + if char in '([{': + depth += 1 + current.append(char) + elif char in ')]}': + depth -= 1 + current.append(char) + elif char == ',' and depth == 0: + params.append(''.join(current).strip()) + current = [] + else: + current.append(char) + if current: + params.append(''.join(current).strip()) + + result = [] + for param in params: + if not param: + continue + param = param.strip() + # Skip self + if param == 'self': + continue + # Skip *args and **kwargs - they need special handling + if param.startswith('*'): + continue + + # Handle Cython typed parameters: Type name [not None] [= default] + # Examples: Row row not None, Variable var not None, int x, object y = None + # The pattern is: [Type] name [not None] [= default] + + # First, check for default value + has_default = '=' in param + + # Remove "not None" modifier if present + param_clean = re.sub(r'\s+not\s+None\s*', ' ', param) + + # Try to match: Type name [= default] or just name [= default] + # Cython type pattern: CapitalizedType name + cython_match = re.match(r'^([A-Z]\w*)\s+(\w+)(?:\s*=.*)?$', param_clean.strip()) + if cython_match: + param_name = cython_match.group(2) + result.append((param_name, has_default)) + continue + + # Try simple Cython type: lowercase_type name (like int x, object y) + simple_cython_match = re.match(r'^([a-z]\w*)\s+(\w+)(?:\s*=.*)?$', param_clean.strip()) + if simple_cython_match: + param_name = simple_cython_match.group(2) + result.append((param_name, has_default)) + continue + + # Python-style: name [: type] [= default] + python_match = re.match(r'^(\w+)(?:\s*:.*)?(?:\s*=.*)?$', param_clean.strip()) + if python_match: + param_name = python_match.group(1) + result.append((param_name, has_default)) + + return result + def _detect_dataclass(self, content: str) -> dict: """Detect @dataclass decorated classes and their fields.""" dataclasses = {} @@ -481,11 +634,15 @@ def generate_stub(self) -> str: lines = [] # Header imports - lines.append('from dataclasses import dataclass') + # Only import dataclass if any class uses it + has_dataclass = any(cls.is_dataclass for cls in self.module_info.classes.values()) + if has_dataclass: + lines.append('from dataclasses import dataclass') lines.append('from typing import ClassVar') lines.append('') lines.append('import numpy') lines.append('from _typeshed import Incomplete') + lines.append('from typing_extensions import disjoint_base') lines.append('') # Module-level variables and functions (sorted alphabetically) @@ -530,10 +687,18 @@ def _generate_class_stub(self, cls_info: ClassInfo) -> list: """Generate stub for a single class.""" lines = [] + # Special handling for Statistics class (has read-only @property methods) + if cls_info.is_statistics_class: + return self._generate_statistics_stub() + # Handle dataclass if cls_info.is_dataclass: lines.append('@dataclass') + # Add @disjoint_base decorator for appropriate classes + if cls_info.name in self.DISJOINT_BASE_CLASSES: + lines.append('@disjoint_base') + # Class declaration if cls_info.parent: lines.append(f'class {cls_info.name}({cls_info.parent}):') @@ -547,6 +712,12 @@ def _generate_class_stub(self, cls_info: ClassInfo) -> list: lines.append(f' {fname}: {ftype} = {fdefault}') else: lines.append(f' {fname}: {ftype}') + # Also include additional attributes (like @property-based ones) + # Use ... as default to allow them after fields with defaults + if cls_info.attributes: + sorted_attrs = sorted(cls_info.attributes, key=lambda x: x[0]) + for attr_name, type_hint in sorted_attrs: + lines.append(f' {attr_name}: {type_hint} = ...') return lines # Class variables (for enum-like classes) @@ -564,7 +735,7 @@ def _generate_class_stub(self, cls_info: ClassInfo) -> list: special_methods = [] comparison_methods = [] - for method in cls_info.methods: + for method in cls_info.methods.keys(): if method in self.COMPARISON_METHODS: comparison_methods.append(method) elif method.startswith('__') and method.endswith('__'): @@ -585,32 +756,64 @@ def _generate_class_stub(self, cls_info: ClassInfo) -> list: comparison_methods.append(cmp_method) # Output __init__ first if present or needs to be added (not for numpy subclasses or specific classes) + # Cython extension types need *args, **kwargs for __init__ + is_cython_extension = cls_info.name in self.CYTHON_EXTENSION_TYPES + has_custom_init = cls_info.name in self.CUSTOM_INIT if '__init__' in special_methods: - lines.append(f' {self.SPECIAL_METHODS["__init__"]}') + if has_custom_init: + lines.append(f' {self.CUSTOM_INIT[cls_info.name]}') + elif is_cython_extension: + # Cython extension types have *args, **kwargs at runtime + lines.append(f' {self.SPECIAL_METHODS["__init__"]}') + else: + params = cls_info.methods.get('__init__', []) + stub = self._generate_method_stub('__init__', params, return_type='None') + lines.append(f' {stub}') special_methods.remove('__init__') elif '__init__' not in cls_info.methods and not is_numpy_subclass and not cls_info.is_dataclass and cls_info.name not in skip_init_classes: - lines.append(f' {self.SPECIAL_METHODS["__init__"]}') + if has_custom_init: + lines.append(f' {self.CUSTOM_INIT[cls_info.name]}') + elif is_cython_extension: + # Cython extension types have *args, **kwargs at runtime + lines.append(f' {self.SPECIAL_METHODS["__init__"]}') + else: + lines.append(f' def __init__(self) -> None: ...') # Sort and output regular methods for method in sorted(regular_methods): + params = cls_info.methods.get(method, []) if method in cls_info.static_methods: lines.append(' @staticmethod') - lines.append(f' def {method}(*args, **kwargs): ...') + stub = self._generate_method_stub(method, params, is_static=True) + lines.append(f' {stub}') else: - lines.append(f' def {method}(self, *args, **kwargs): ...') + stub = self._generate_method_stub(method, params) + # Add type: ignore[override] for numpy subclass override methods + if is_numpy_subclass and method in self.NUMPY_OVERRIDE_METHODS: + stub = stub.replace(': ...', ': ... # type: ignore[override]') + lines.append(f' {stub}') # Combine special methods and comparison methods, sort alphabetically all_special = [] for method in special_methods: if method in self.SPECIAL_METHODS: - all_special.append((method, self.SPECIAL_METHODS[method])) + stub = self.SPECIAL_METHODS[method] elif method in self.TYPED_METHODS: - all_special.append((method, self.TYPED_METHODS[method])) + stub = self.TYPED_METHODS[method] else: - all_special.append((method, f'def {method}(self, *args, **kwargs): ...')) + params = cls_info.methods.get(method, []) + stub = self._generate_method_stub(method, params) + # Add type: ignore[override] for numpy subclass override methods + if is_numpy_subclass and method in self.NUMPY_OVERRIDE_METHODS: + stub = stub.replace(': ...', ': ... # type: ignore[override]') + all_special.append((method, stub)) for method in comparison_methods: - all_special.append((method, self.COMPARISON_METHODS[method])) + stub = self.COMPARISON_METHODS[method] + # Add type: ignore[override] for numpy subclass override methods + if is_numpy_subclass and method in self.NUMPY_OVERRIDE_METHODS: + stub = stub.replace(': ...', ': ... # type: ignore[override]') + all_special.append((method, stub)) # Sort and output all special methods alphabetically for method, stub in sorted(all_special, key=lambda x: x[0]): @@ -622,6 +825,70 @@ def _generate_class_stub(self, cls_info: ClassInfo) -> list: return lines + def _generate_statistics_stub(self) -> list: + """Generate stub for the Statistics class with proper @property decorators.""" + lines = [] + lines.append('class Statistics:') + + # Writable attributes (from __init__ parameters) + writable_attrs = [ + ('status', 'str'), + ('total_time', 'float'), + ('solving_time', 'float'), + ('presolving_time', 'float'), + ('reading_time', 'float'), + ('copying_time', 'float'), + ('problem_name', 'str'), + ('presolved_problem_name', 'str'), + ('n_runs', 'int | None'), + ('n_nodes', 'int | None'), + ('n_solutions_found', 'int'), + ('first_solution', 'float | None'), + ('primal_bound', 'float | None'), + ('dual_bound', 'float | None'), + ('gap', 'float | None'), + ('primal_dual_integral', 'float | None'), + ] + + # Read-only properties (derived from other fields) + readonly_props = [ + ('n_binary_vars', 'int'), + ('n_conss', 'int'), + ('n_continuous_vars', 'int'), + ('n_implicit_integer_vars', 'int'), + ('n_integer_vars', 'int'), + ('n_maximal_cons', 'int'), + ('n_presolved_binary_vars', 'int'), + ('n_presolved_conss', 'int'), + ('n_presolved_continuous_vars', 'int'), + ('n_presolved_implicit_integer_vars', 'int'), + ('n_presolved_integer_vars', 'int'), + ('n_presolved_maximal_cons', 'int'), + ('n_presolved_vars', 'int'), + ('n_vars', 'int'), + ] + + # Output writable attributes + for attr_name, attr_type in writable_attrs: + lines.append(f' {attr_name}: {attr_type}') + + # Output __init__ method + init_params = [] + for attr_name, attr_type in writable_attrs: + init_params.append(f'{attr_name}: {attr_type}') + init_sig = ', '.join(['self'] + init_params) + lines.append(f' def __init__({init_sig}) -> None: ...') + + # Output read-only properties with @property decorator + for prop_name, prop_type in readonly_props: + lines.append(' @property') + lines.append(f' def {prop_name}(self) -> {prop_type}: ...') + + # Statistics is a dataclass at runtime, so it has __replace__ + lines.append(' def __replace__(self, **changes: Incomplete) -> Statistics: ...') + + return lines + def run(self) -> str: """Run the stub generator on all relevant files.""" pxi_files = list(self.src_dir.glob('*.pxi')) @@ -664,27 +931,20 @@ def _apply_special_cases(self) -> None: if ('name', 'Incomplete') not in cls_info.attributes: cls_info.attributes.append(('name', 'Incomplete')) - # Handle Statistics as dataclass + # Handle Statistics - has both dataclass fields and @property methods + # We need to use explicit @property decorators for read-only attributes if 'Statistics' in self.module_info.classes: cls_info = self.module_info.classes['Statistics'] - cls_info.is_dataclass = True - cls_info.dataclass_fields = [ - ('status', 'str', None), - ('total_time', 'float', None), - ('solving_time', 'float', None), - ('presolving_time', 'float', None), - ('reading_time', 'float', None), - ('copying_time', 'float', None), - ('problem_name', 'str', None), - ('presolved_problem_name', 'str', None), - ('n_runs', 'int', 'None'), - ('n_nodes', 'int', 'None'), - ('n_solutions_found', 'int', '-1'), - ('first_solution', 'float', 'None'), - ('primal_bound', 'float', 'None'), - ('dual_bound', 'float', 'None'), - ('gap', 'float', 'None'), - ] + # Clear any parsed attributes - we handle Statistics fully manually + cls_info.attributes = [] + cls_info.methods = {} # Clear methods, we'll generate them manually + # Mark as special class that needs custom generation + cls_info.is_statistics_class = True + + # Handle Term class - has *vartuple in __init__ + if 'Term' in self.module_info.classes: + cls_info = self.module_info.classes['Term'] + cls_info.methods['__init__'] = [('vartuple', False)] # Will be handled specially # Add/update known module-level variables with correct types known_vars = { diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index b9bffc1d6..352bd9b28 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -737,6 +737,7 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPepsilon(SCIP* scip) SCIP_Real SCIPfeastol(SCIP* scip) SCIP_RETCODE SCIPsetObjIntegral(SCIP* scip) + SCIP_Bool SCIPisObjIntegral(SCIP* scip) SCIP_Real SCIPgetLocalOrigEstimate(SCIP* scip) SCIP_Real SCIPgetLocalTransEstimate(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index da23028f9..f08746c35 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -3981,6 +3981,17 @@ cdef class Model: """ PY_SCIP_CALL(SCIPsetObjIntegral(self._scip)) + def isObjIntegral(self): + """ + Returns whether the objective function is integral. + + Returns + ------- + bool + + """ + return SCIPisObjIntegral(self._scip) + def getLocalEstimate(self, original = False): """ Gets estimate of best primal solution w.r.t. original or transformed problem contained in current subtree. diff --git a/src/pyscipopt/scip.pyi b/src/pyscipopt/scip.pyi index 831dd02ed..ef6fce23f 100644 --- a/src/pyscipopt/scip.pyi +++ b/src/pyscipopt/scip.pyi @@ -1,4 +1,3 @@ -from dataclasses import dataclass from typing import ClassVar import numpy @@ -119,13 +118,13 @@ class Column: def getUb(self) -> Incomplete: ... def getVar(self) -> Incomplete: ... def isIntegral(self) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... def __hash__(self) -> int: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... @disjoint_base class ColumnExact: @@ -257,13 +256,13 @@ class Constraint: def isRemovable(self) -> Incomplete: ... def isSeparated(self) -> Incomplete: ... def isStickingAtNode(self) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... def __hash__(self) -> int: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... @disjoint_base class Cutsel: @@ -300,13 +299,13 @@ class Event: def getRow(self) -> Incomplete: ... def getType(self) -> Incomplete: ... def getVar(self) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... def __hash__(self) -> int: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... @disjoint_base class Eventhdlr: @@ -325,24 +324,24 @@ class Eventhdlr: @disjoint_base class Expr: terms: Incomplete - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... + def __init__(self, terms: Incomplete = ...) -> None: ... def degree(self) -> Incomplete: ... def normalize(self) -> Incomplete: ... def __abs__(self) -> Incomplete: ... def __add__(self, other: Incomplete) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... def __getitem__(self, index: Incomplete) -> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __gt__(self, other: object) -> bool: ... def __iadd__(self, other: Incomplete) -> Incomplete: ... # noqa: PYI034 def __iter__(self) -> Incomplete: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... def __mul__(self, other: Incomplete) -> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __ne__(self, other: object) -> bool: ... def __neg__(self) -> Incomplete: ... def __next__(self) -> Incomplete: ... - def __pow__(self, other: Incomplete, mod: Incomplete = ...) -> Incomplete: ... + def __pow__(self, other: Incomplete, modulo: Incomplete = ...) -> Incomplete: ... def __radd__(self, other: Incomplete) -> Incomplete: ... def __rmul__(self, other: Incomplete) -> Incomplete: ... def __rpow__(self, other: Incomplete) -> Incomplete: ... @@ -356,34 +355,36 @@ class ExprCons: _lhs: Incomplete _rhs: Incomplete expr: Incomplete - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... + def __init__( + self, expr: Incomplete, lhs: Incomplete = ..., rhs: Incomplete = ... + ) -> None: ... def normalize(self) -> Incomplete: ... - def __bool__(self)-> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __bool__(self) -> bool: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... @disjoint_base class GenExpr: _op: Incomplete children: Incomplete - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... + def __init__(self) -> None: ... def degree(self) -> Incomplete: ... def getOp(self) -> Incomplete: ... def __abs__(self) -> Incomplete: ... def __add__(self, other: Incomplete) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... def __mul__(self, other: Incomplete) -> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __ne__(self, other: object) -> bool: ... def __neg__(self) -> Incomplete: ... - def __pow__(self, other: Incomplete, mod: Incomplete = ...) -> Incomplete: ... + def __pow__(self, other: Incomplete, modulo: Incomplete = ...) -> Incomplete: ... def __radd__(self, other: Incomplete) -> Incomplete: ... def __rmul__(self, other: Incomplete) -> Incomplete: ... def __rpow__(self, other: Incomplete) -> Incomplete: ... @@ -428,7 +429,7 @@ class IISfinder: @disjoint_base class LP: name: Incomplete - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... + def __init__(self, name: Incomplete = ..., sense: Incomplete = ...) -> None: ... def addCol( self, entries: Incomplete, @@ -509,14 +510,12 @@ class MatrixConstraint(numpy.ndarray): def isStickingAtNode(self) -> Incomplete: ... class MatrixExpr(numpy.ndarray): - def sum( # type: ignore[override] - self, axis: Incomplete = ..., keepdims: Incomplete = ..., **kwargs: Incomplete - ) -> Incomplete: ... + def sum(self, axis: Incomplete = ..., keepdims: Incomplete = ...) -> Incomplete: ... # type: ignore[override] def __add__(self, other: Incomplete) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete) -> MatrixExprCons: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... # type: ignore[override] def __iadd__(self, other: Incomplete) -> Incomplete: ... # noqa: PYI034 - def __le__(self, other: Incomplete) -> MatrixExprCons: ... + def __le__(self, other: object) -> bool: ... # type: ignore[override] def __matmul__(self, other: Incomplete) -> Incomplete: ... def __mul__(self, other: Incomplete) -> Incomplete: ... def __pow__(self, other: Incomplete) -> Incomplete: ... # type: ignore[override] @@ -528,12 +527,11 @@ class MatrixExpr(numpy.ndarray): def __truediv__(self, other: Incomplete) -> Incomplete: ... class MatrixExprCons(numpy.ndarray): - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete) -> MatrixExprCons: ... - def __le__(self, other: Incomplete) -> MatrixExprCons: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... # type: ignore[override] + def __le__(self, other: object) -> bool: ... # type: ignore[override] -class MatrixGenExpr(MatrixExpr): - ... +class MatrixGenExpr(MatrixExpr): ... class MatrixVariable(MatrixExpr): def getAvgSol(self) -> Incomplete: ... @@ -555,19 +553,21 @@ class MatrixVariable(MatrixExpr): class Model: _freescip: Incomplete data: Incomplete - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... - def _createConsGenNonlinear( - self, cons: Incomplete, **kwargs: Incomplete - ) -> Incomplete: ... - def _createConsLinear( - self, lincons: Incomplete, **kwargs: Incomplete - ) -> Incomplete: ... - def _createConsNonlinear( - self, cons: Incomplete, **kwargs: Incomplete - ) -> Incomplete: ... - def _createConsQuadratic( - self, quadcons: Incomplete, **kwargs: Incomplete - ) -> Incomplete: ... + def __init__( + self, + problemName: Incomplete = ..., + defaultPlugins: Incomplete = ..., + sourceModel: Incomplete = ..., + origcopy: Incomplete = ..., + globalcopy: Incomplete = ..., + enablepricing: Incomplete = ..., + createscip: Incomplete = ..., + threadsafe: Incomplete = ..., + ) -> None: ... + def _createConsGenNonlinear(self, cons: Incomplete) -> Incomplete: ... + def _createConsLinear(self, lincons: Incomplete) -> Incomplete: ... + def _createConsNonlinear(self, cons: Incomplete) -> Incomplete: ... + def _createConsQuadratic(self, quadcons: Incomplete) -> Incomplete: ... def _getStageNames(self) -> Incomplete: ... def activateBenders( self, benders: Incomplete, nsubproblems: Incomplete @@ -1386,6 +1386,7 @@ class Model: def isNLPConstructed(self) -> Incomplete: ... def isNegative(self, val: Incomplete) -> Incomplete: ... def isObjChangedProbing(self) -> Incomplete: ... + def isObjIntegral(self) -> Incomplete: ... def isPositive(self, val: Incomplete) -> Incomplete: ... def isZero(self, value: Incomplete) -> Incomplete: ... def lpiGetIterations(self) -> Incomplete: ... @@ -1574,13 +1575,13 @@ class Model: filename: Incomplete = ..., write_zeros: Incomplete = ..., ) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... def __hash__(self) -> int: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... @disjoint_base class NLRow: @@ -1592,13 +1593,13 @@ class NLRow: def getLhs(self) -> Incomplete: ... def getLinearTerms(self) -> Incomplete: ... def getRhs(self) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... def __hash__(self) -> int: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... @disjoint_base class Node: @@ -1618,13 +1619,13 @@ class Node: def getType(self) -> Incomplete: ... def isActive(self) -> Incomplete: ... def isPropagatedAgain(self) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... def __hash__(self) -> int: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... @disjoint_base class Nodesel: @@ -2034,13 +2035,13 @@ class Row: def isLocal(self) -> Incomplete: ... def isModifiable(self) -> Incomplete: ... def isRemovable(self) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... + def __gt__(self, other: object) -> bool: ... def __hash__(self) -> int: ... - def __le__(self, other: Incomplete)-> Incomplete: ... - def __lt__(self, other: Incomplete)-> Incomplete: ... - def __ne__(self, other: Incomplete)-> Incomplete: ... + def __le__(self, other: object) -> bool: ... + def __lt__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... @disjoint_base class RowExact: @@ -2063,16 +2064,15 @@ class Sepa: @disjoint_base class Solution: data: Incomplete - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... + def __init__(self, raise_error: Incomplete = ...) -> None: ... def _checkStage(self, method: Incomplete) -> Incomplete: ... def _evaluate(self, term: Incomplete) -> Incomplete: ... def getOrigin(self) -> Incomplete: ... def retransform(self) -> Incomplete: ... def translate(self, target: Incomplete) -> Incomplete: ... def __getitem__(self, index: Incomplete) -> Incomplete: ... - def __setitem__(self, index: Incomplete, object: Incomplete) -> None: ... + def __setitem__(self, key: Incomplete, value: Incomplete) -> None: ... -@dataclass class Statistics: status: str total_time: float @@ -2082,42 +2082,62 @@ class Statistics: copying_time: float problem_name: str presolved_problem_name: str - n_runs: int | None = None - n_nodes: int | None = None - n_solutions_found: int = -1 - first_solution: float | None = None - primal_bound: float | None = None - dual_bound: float | None = None - gap: float | None = None - primal_dual_integral: float | None = None + n_runs: int | None + n_nodes: int | None + n_solutions_found: int + first_solution: float | None + primal_bound: float | None + dual_bound: float | None + gap: float | None + primal_dual_integral: float | None + def __init__( + self, + status: str, + total_time: float, + solving_time: float, + presolving_time: float, + reading_time: float, + copying_time: float, + problem_name: str, + presolved_problem_name: str, + n_runs: int | None, + n_nodes: int | None, + n_solutions_found: int, + first_solution: float | None, + primal_bound: float | None, + dual_bound: float | None, + gap: float | None, + primal_dual_integral: float | None, + ) -> None: ... @property - def n_binary_vars(self) -> Incomplete: ... + def n_binary_vars(self) -> int: ... @property - def n_conss(self) -> Incomplete: ... + def n_conss(self) -> int: ... @property - def n_continuous_vars(self) -> Incomplete: ... + def n_continuous_vars(self) -> int: ... @property - def n_implicit_integer_vars(self) -> Incomplete: ... + def n_implicit_integer_vars(self) -> int: ... @property - def n_integer_vars(self) -> Incomplete: ... + def n_integer_vars(self) -> int: ... @property - def n_maximal_cons(self) -> Incomplete: ... + def n_maximal_cons(self) -> int: ... @property - def n_presolved_binary_vars(self) -> Incomplete: ... + def n_presolved_binary_vars(self) -> int: ... @property - def n_presolved_conss(self) -> Incomplete: ... + def n_presolved_conss(self) -> int: ... @property - def n_presolved_continuous_vars(self) -> Incomplete: ... + def n_presolved_continuous_vars(self) -> int: ... @property - def n_presolved_implicit_integer_vars(self) -> Incomplete: ... + def n_presolved_implicit_integer_vars(self) -> int: ... @property - def n_presolved_integer_vars(self) -> Incomplete: ... + def n_presolved_integer_vars(self) -> int: ... @property - def n_presolved_maximal_cons(self) -> Incomplete: ... + def n_presolved_maximal_cons(self) -> int: ... @property - def n_presolved_vars(self) -> Incomplete: ... + def n_presolved_vars(self) -> int: ... @property - def n_vars(self) -> Incomplete: ... + def n_vars(self) -> int: ... + def __replace__(self, **changes: Incomplete) -> Statistics: ... @disjoint_base class SumExpr(GenExpr): @@ -2129,12 +2149,12 @@ class Term: hashval: Incomplete ptrtuple: Incomplete vartuple: Incomplete - def __init__(self, *args: Incomplete) -> None: ... + def __init__(self, *vartuple: Incomplete) -> None: ... def __add__(self, other: Incomplete) -> Incomplete: ... - def __eq__(self, other: Incomplete)-> Incomplete: ... - def __ge__(self, other: Incomplete)-> Incomplete: ... + def __eq__(self, other: object) -> bool: ... + def __ge__(self, other: object) -> bool: ... def __getitem__(self, index: Incomplete) -> Incomplete: ... - def __gt__(self, other: Incomplete)-> Incomplete: ... + def __gt__(self, other: object) -> bool: ... def __hash__(self) -> int: ... def __le__(self, other: object) -> bool: ... def __len__(self) -> int: ... @@ -2153,7 +2173,7 @@ class VarExpr(GenExpr): class Variable(Expr): data: Incomplete name: Incomplete - def __init__(self, *args: Incomplete, **kwargs: Incomplete) -> None: ... + def __init__(self) -> None: ... def getAvgSol(self) -> Incomplete: ... def getCol(self) -> Incomplete: ... def getImplType(self) -> Incomplete: ... @@ -2186,3 +2206,7 @@ class Variable(Expr): def ptr(self) -> Incomplete: ... def varMayRound(self, direction: Incomplete = ...) -> Incomplete: ... def vtype(self) -> Incomplete: ... + +@disjoint_base +class _VarArray: + def __init__(self) -> None: ... diff --git a/tests/test_model.py b/tests/test_model.py index 573a507e3..fbb3ff563 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -563,4 +563,15 @@ def test_getVarPseudocost(): p = m.getVarPseudocost(var, SCIP_BRANCHDIR.UPWARDS) # Not exactly 12 because the new value is a weighted sum of all the updates - assert m.isEQ(p, 12.0001) \ No newline at end of file + assert m.isEQ(p, 12.0001) + +def test_objIntegral(): + m = Model() + m.setObjIntegral() + assert m.isObjIntegral() + + m = Model() + x = m.addVar(vtype='C', obj=1.5) + m.addCons(x >= 0) + m.optimize() + assert not m.isObjIntegral() \ No newline at end of file