Skip to content

Commit 931b6b5

Browse files
committed
Add experimental support for verbose output and writing stderr to file
1 parent c294392 commit 931b6b5

File tree

4 files changed

+108
-26
lines changed

4 files changed

+108
-26
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Added
1313
^^^^^
1414
- Support for Python 3.9. (MiniZinc Python will aim to support all versions of
1515
Python that are not deprecated)
16+
- Experimental support for capturing the error output of the MiniZinc process
17+
in ``CLIInstance``.
18+
- Experimental support for verbose compiler and solver output (using the ``-v``
19+
flag) in ``CLIInstance``.
1620

1721
Changed
1822
^^^^^^^

docs/advanced_usage.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,53 @@ The generated file will contain all remote calls to the MiniZinc executable:
344344
DEBUG:minizinc:CLIInstance:analyse -> output fields: [('q', typing.List[int]), ('_checker', <class 'str'>)]
345345
DEBUG:asyncio:Using selector: KqueueSelector
346346
DEBUG:minizinc:CLIDriver:create_process -> program: /Applications/MiniZincIDE.app/Contents/Resources/minizinc args: "--solver [email protected] --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s nqueens.mzn /var/folders/gj/cmhh026j5ddb28sw1z95pygdy5kk20/T/mzn_datamnhikzwo.json"
347+
348+
349+
If MiniZinc Python instances are instantiated directly from Python, then the
350+
CLI driver will generate temporary files to use with the created MiniZinc
351+
process. When something seems wrong with MiniZinc Python it is often a good
352+
idea to retrieve these objects to both check if the files are generated
353+
correctly and to recreate the exact MiniZinc command that is ran. For a
354+
``CLIInstance`` object, you can use the ``files()`` method to generate the
355+
files. You can then inspect or copy these files:
356+
357+
.. code-block:: python
358+
359+
import minizinc
360+
from pathlib import Path
361+
362+
model = minizinc.Model(["nqueens.mzn"])
363+
solver = minizinc.Solver.lookup("gecode")
364+
instance = minizinc.Instance(solver, model)
365+
instance["n"] = 9
366+
367+
with instance.files() as files:
368+
store = Path("./tmp")
369+
store.mkdir()
370+
for f in files:
371+
f.link_to(store / f.name)
372+
373+
374+
Finally, if you are looking for bugs related to the behaviour of MiniZinc,
375+
solvers, or solver libraries, then it could be helpful to have a look at the
376+
direct MiniZinc output on ``stderr``. The ``CLIInstance`` class now has
377+
experimental support for to write the output from ``stderr`` to a file and to
378+
enable verbose compilation and solving (using the command line ``-v`` flag).
379+
The former is enable by providing a ``pathlib.Path`` object as the
380+
``debug_output`` parameter to any solving method. Similarly, the verbose flag
381+
is triggered by setting the ``verbose`` parameter to ``True``. The following
382+
fragment shows capture the verbose output of a model that contains trace
383+
statements (which are also captured on ``stderr``):
384+
385+
.. code-block:: python
386+
387+
import minizinc
388+
from pathlib import Path
389+
390+
gecode = minizinc.Solver.lookup("gecode")
391+
instance = minizinc.Instance(gecode)
392+
instance.add_string("""
393+
constraint trace("--- TRACE: This is a trace statement\\n");
394+
""")
395+
396+
instance.solve(verbose=True, debug_output=Path("./debug_output.txt"))

src/minizinc/CLI/instance.py

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import sys
1111
import tempfile
1212
import warnings
13+
from asyncio import StreamReader
1314
from dataclasses import field, make_dataclass
1415
from datetime import datetime, timedelta
1516
from enum import EnumMeta
@@ -253,7 +254,8 @@ async def solutions(
253254
intermediate_solutions=False,
254255
free_search: bool = False,
255256
optimisation_level: Optional[int] = None,
256-
ignore_errors=False,
257+
verbose: bool = False,
258+
debug_output: Optional[Path] = None,
257259
**kwargs,
258260
):
259261
# Set standard command line arguments
@@ -319,6 +321,9 @@ async def solutions(
319321
if timeout is not None:
320322
cmd.extend(["--time-limit", str(int(timeout.total_seconds() * 1000))])
321323

324+
if verbose:
325+
cmd.append("-v")
326+
322327
for flag, value in kwargs.items():
323328
if not flag.startswith("-"):
324329
flag = "--" + flag
@@ -334,35 +339,23 @@ async def solutions(
334339
cmd.extend(files)
335340
# Run the MiniZinc process
336341
proc = await self._driver.create_process(cmd, solver=solver)
337-
assert proc.stderr is not None
338-
assert proc.stdout is not None
342+
assert isinstance(proc.stderr, StreamReader)
343+
assert isinstance(proc.stdout, StreamReader)
344+
345+
# Python 3.7+: replace with asyncio.create_task
346+
read_stderr = asyncio.ensure_future(_read_all(proc.stderr))
339347

340348
status = Status.UNKNOWN
341349
code = 0
342-
deadline = None
343-
if timeout is not None:
344-
deadline = datetime.now() + timeout + timedelta(seconds=5)
345350

346351
remainder: Optional[bytes] = None
347352
try:
348-
raw_sol: bytes = b""
349-
while not proc.stdout.at_eof():
350-
try:
351-
if deadline is None:
352-
raw_sol += await proc.stdout.readuntil(SEPARATOR)
353-
else:
354-
t = deadline - datetime.now()
355-
raw_sol += await asyncio.wait_for(
356-
proc.stdout.readuntil(SEPARATOR), t.total_seconds()
357-
)
358-
status = Status.SATISFIED
359-
solution, statistics = parse_solution(
360-
raw_sol, self.output_type, self._enum_map
361-
)
362-
yield Result(Status.SATISFIED, solution, statistics)
363-
raw_sol = b""
364-
except asyncio.LimitOverrunError as err:
365-
raw_sol += await proc.stdout.readexactly(err.consumed)
353+
async for raw_sol in _seperate_solutions(proc.stdout, timeout):
354+
status = Status.SATISFIED
355+
solution, statistics = parse_solution(
356+
raw_sol, self.output_type, self._enum_map
357+
)
358+
yield Result(Status.SATISFIED, solution, statistics)
366359

367360
code = await proc.wait()
368361
except asyncio.IncompleteReadError as err:
@@ -391,9 +384,14 @@ async def solutions(
391384
)
392385
yield Result(status, solution, statistics)
393386
# Raise error if required
387+
stderr = None
394388
if code != 0 or status == Status.ERROR:
395-
stderr = await proc.stderr.read()
389+
stderr = await read_stderr
396390
raise parse_error(stderr)
391+
if debug_output is not None:
392+
if stderr is None:
393+
stderr = await read_stderr
394+
debug_output.write_bytes(stderr)
397395

398396
async def solve_async(
399397
self,
@@ -514,3 +512,34 @@ def add_file(self, file: ParPath, parse_data: bool = True) -> None:
514512
def add_string(self, code: str) -> None:
515513
self._reset_analysis()
516514
return super().add_string(code)
515+
516+
517+
async def _seperate_solutions(stream: StreamReader, timeout: Optional[timedelta]):
518+
deadline = None
519+
if timeout is not None:
520+
deadline = datetime.now() + timeout + timedelta(seconds=1)
521+
solution: bytes = b""
522+
while not stream.at_eof():
523+
try:
524+
if deadline is None:
525+
solution += await stream.readuntil(SEPARATOR)
526+
else:
527+
t = deadline - datetime.now()
528+
solution += await asyncio.wait_for(
529+
stream.readuntil(SEPARATOR), t.total_seconds()
530+
)
531+
yield solution
532+
solution = b""
533+
except asyncio.LimitOverrunError as err:
534+
solution += await stream.readexactly(err.consumed)
535+
536+
537+
async def _read_all(stream: StreamReader):
538+
output: bytes = b""
539+
while not stream.at_eof():
540+
try:
541+
output += await stream.read()
542+
return output
543+
except asyncio.LimitOverrunError as err:
544+
output += await stream.readexactly(err.consumed)
545+
return output

src/minizinc/instance.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ async def solutions(
170170
intermediate_solutions=False,
171171
free_search: bool = False,
172172
optimisation_level: Optional[int] = None,
173-
ignore_errors=False,
174173
**kwargs,
175174
):
176175
"""An asynchronous generator for solutions of the MiniZinc instance.

0 commit comments

Comments
 (0)