From fadb0a4f74bc29fa816730e6fb4d5a56e269c4c6 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Sat, 23 May 2026 21:35:33 +0400 Subject: [PATCH 01/10] Use Click to parse command-line args in lemke.py --- pyproject.toml | 3 +- src/lemke/lemke.py | 86 +++++++++++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2538a63..d63f946 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,8 @@ name = "lemke" version = "0.0.1" dependencies = [ "numpy>=2.2,<2.3", - "matplotlib>=3.10,<3.11" + "matplotlib>=3.10,<3.11", + "click>=8.1", ] [project.optional-dependencies] diff --git a/src/lemke/lemke.py b/src/lemke/lemke.py index e0a1013..3e88598 100644 --- a/src/lemke/lemke.py +++ b/src/lemke/lemke.py @@ -4,45 +4,12 @@ import math # gcd import sys +import click + from . import columnprint, utils # global defaults -lcpfilename = "lcp" -outfile = lcpfilename + ".out" filehandle = sys.stdout -verbose = False -silent = False -z0 = False - - -# process command-line arguments -def processArguments(): - global lcpfilename, outfile, filehandle, verbose, silent, z0 - helpstring = """usage: lemke.py [options] -options: -v, -verbose : printout intermediate tableaus - -s, -silent : send output to .out - -z0 : show value of z0 at each step - -?, -help: show this help - (default: "lcp", must not start with "-") - Example: [python] lemke.py -v 2lcp""" - arglist = sys.argv[1:] - showhelp = False - for s in arglist: - if s == "-v" or s == "-verbose": - verbose = True - elif s == "-s" or s == "-silent": - silent = True - elif s == "-z0": - z0 = True - elif s[0] == "-": - showhelp = True - else: - lcpfilename = s - outfile = s + ".out" - if showhelp: - printout(helpstring) - exit(0) - return def printout(*s): @@ -468,7 +435,15 @@ def pivot(self, leave, enter): # end of pivot (leave, enter) - def runlemke(self, *, verbose=False, lexstats=False, z0=False, silent=False): + def runlemke( + self, + *, + verbose=False, + lexstats=False, + z0=False, + silent=False, + lcpfilename="lcp", + ): global filehandle # z0: printout value of z0 # flags.maxcount = 0; @@ -479,6 +454,8 @@ def runlemke(self, *, verbose=False, lexstats=False, z0=False, silent=False): # flags.binteract = 0; # flags.blexstats = 0; + outfile = lcpfilename + ".out" + if silent: filehandle = open(outfile, "w") # noqa: SIM115 n = self.n @@ -532,15 +509,46 @@ def runlemke(self, *, verbose=False, lexstats=False, z0=False, silent=False): # end of class tableau -def main(): - processArguments() +@click.command( + context_settings={"help_option_names": ["-?", "--help"]}, +) +@click.option( + "-v", + "--verbose", + is_flag=True, + help="Printout intermediate tableaus", +) +@click.option( + "-s", + "--silent", + is_flag=True, + help="Send output to '[LCPFILENAME].out'", +) +@click.option( + "--z0", + is_flag=True, + help="Show value of z0 at each step", +) +@click.argument( + "lcpfilename", + default="lcp", + required=False, + type=click.Path(exists=True, readable=True), +) +def main(verbose, silent, z0, lcpfilename): + """ + Tool for solving linear complementarity problems using Lemke's algorithm. + + [LCPFILENAME] is the path to the input file (default: "lcp") + """ + printout(f"verbose={verbose} lcpfilename={lcpfilename} silent={silent} z0={z0}") # printout (f"{verbose}= {lcpfilename}= {silent}= {z0}=") m = lcp(lcpfilename) printout(m) printout("==================================") tabl = tableau(m) - tabl.runlemke(verbose=verbose, z0=z0, silent=silent) + tabl.runlemke(verbose=verbose, z0=z0, silent=silent, lcpfilename=lcpfilename) if __name__ == "__main__": From fd77634e4fc5fe09904d8088537c9d9209a6c28c Mon Sep 17 00:00:00 2001 From: nataliemes <159729183+nataliemes@users.noreply.github.com> Date: Sun, 24 May 2026 14:42:02 +0400 Subject: [PATCH 02/10] Add upper bound to Click dependency Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d63f946..723915f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.0.1" dependencies = [ "numpy>=2.2,<2.3", "matplotlib>=3.10,<3.11", - "click>=8.1", + "click>=8.1,<9", ] [project.optional-dependencies] From 76547eec20e8cdf27c8d6aaa87b0bc6027286b9a Mon Sep 17 00:00:00 2001 From: nataliemes Date: Sun, 24 May 2026 14:51:04 +0400 Subject: [PATCH 03/10] Ensure "lcpfilename" argument accepts only files --- src/lemke/lemke.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lemke/lemke.py b/src/lemke/lemke.py index 3e88598..a4c3a65 100644 --- a/src/lemke/lemke.py +++ b/src/lemke/lemke.py @@ -533,7 +533,7 @@ def runlemke( "lcpfilename", default="lcp", required=False, - type=click.Path(exists=True, readable=True), + type=click.Path(exists=True, readable=True, file_okay=True, dir_okay=False), ) def main(verbose, silent, z0, lcpfilename): """ From 04a3e94fa85f9a33fa29d8b0437579ddfa64d040 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Sun, 24 May 2026 14:52:50 +0400 Subject: [PATCH 04/10] Add "-h" to help option names --- src/lemke/lemke.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lemke/lemke.py b/src/lemke/lemke.py index a4c3a65..f060ee8 100644 --- a/src/lemke/lemke.py +++ b/src/lemke/lemke.py @@ -510,7 +510,7 @@ def runlemke( @click.command( - context_settings={"help_option_names": ["-?", "--help"]}, + context_settings={"help_option_names": ["-?", "-h", "--help"]}, ) @click.option( "-v", From ab89698218f3fc8e9aa0fb3f5a1fff738a3d5094 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Wed, 27 May 2026 22:52:21 +0400 Subject: [PATCH 05/10] Remove default value for "lcpfilename" argument --- src/lemke/lemke.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lemke/lemke.py b/src/lemke/lemke.py index f060ee8..e18a764 100644 --- a/src/lemke/lemke.py +++ b/src/lemke/lemke.py @@ -442,7 +442,7 @@ def runlemke( lexstats=False, z0=False, silent=False, - lcpfilename="lcp", + lcpfilename, ): global filehandle # z0: printout value of z0 @@ -531,15 +531,13 @@ def runlemke( ) @click.argument( "lcpfilename", - default="lcp", - required=False, type=click.Path(exists=True, readable=True, file_okay=True, dir_okay=False), ) def main(verbose, silent, z0, lcpfilename): """ Tool for solving linear complementarity problems using Lemke's algorithm. - [LCPFILENAME] is the path to the input file (default: "lcp") + [LCPFILENAME] is the path to the input file """ printout(f"verbose={verbose} lcpfilename={lcpfilename} silent={silent} z0={z0}") From f9210956294aebed61b48371f2819e8a696bb240 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Thu, 28 May 2026 21:22:03 +0400 Subject: [PATCH 06/10] Pass output stream explicitly to runlemke() --- src/lemke/bimatrix.py | 4 +- src/lemke/lemke.py | 103 +++++++++++++++++++----------------------- tests/test_lcp.py | 4 +- 3 files changed, 50 insertions(+), 61 deletions(-) diff --git a/src/lemke/bimatrix.py b/src/lemke/bimatrix.py index 241e34c..b66f339 100644 --- a/src/lemke/bimatrix.py +++ b/src/lemke/bimatrix.py @@ -253,7 +253,7 @@ def runLH(self, droppedlabel): lcp.d[droppedlabel - 1] = 0 # subsidize this label tabl = lemke.tableau(lcp) # tabl.runlemke(verbose=True, lexstats=True, z0=gz0) - tabl.runlemke(silent=True) + tabl.runlemke() return tuple(getequil(tabl)) def LH(self, LHstring): @@ -281,7 +281,7 @@ def runtrace(self, xprior, yprior): xB = xprior @ self.B.negmatrix lcp.d = np.hstack((Ay, xB, [1, 1])) tabl = lemke.tableau(lcp) - tabl.runlemke(silent=True) + tabl.runlemke() return tuple(getequil(tabl)) def tracing(self, trace): diff --git a/src/lemke/lemke.py b/src/lemke/lemke.py index e18a764..705913b 100644 --- a/src/lemke/lemke.py +++ b/src/lemke/lemke.py @@ -8,13 +8,6 @@ from . import columnprint, utils -# global defaults -filehandle = sys.stdout - - -def printout(*s): - print(*s, file=filehandle) - # LCP data M,q,d class lcp: @@ -37,9 +30,9 @@ def __init__(self, arg): # flatten into words words = utils.towords(lines) if words[0] != "n=": - printout("lcp file", repr(filename), - "must start with 'n=' lcpdim, e.g. 'n= 5', not", - repr(words[0])) + print("lcp file", repr(filename), + "must start with 'n=' lcpdim, e.g. 'n= 5', not", + repr(words[0])) exit(1) n = int(words[1]) self.n = n @@ -54,9 +47,9 @@ def __init__(self, arg): needfracs = n * n + 2 * n if len(words) != needfracs + 5: # printout("in lcp file '",filename,"':") - printout("in lcp file " + repr(filename) + ":") - printout("n=", n, ", need keywords 'M=' 'q=' 'd=' and n*n + n + n =", - needfracs, "fractions, got", len(words) - 5) + print("in lcp file " + repr(filename) + ":") + print("n=", n, ", need keywords 'M=' 'q=' 'd=' and n*n + n + n =", + needfracs, "fractions, got", len(words) - 5) exit(1) k = 2 # index in words while k < len(words): @@ -73,8 +66,8 @@ def __init__(self, arg): self.d = utils.tovector(n, words, k) k += n else: - printout("in lcp file " + repr(filename) + ":") - printout("expected one of 'M=' 'q=' 'd=', got", repr(words[k])) + print("in lcp file " + repr(filename) + ":") + print("expected one of 'M=' 'q=' 'd=', got", repr(words[k])) exit(1) return @@ -106,6 +99,7 @@ def __str__(self): class tableau: # filling the tableau from the LCP instance Mqd def __init__(self, Mqd): + self.output_stream = None self.n = Mqd.n n = self.n self.scalefactor = [0] * (n + 2) # 0 for z0, n+1 for RHS @@ -233,15 +227,15 @@ def outsol(self): # string giving solution, after createsol() def assertbasic(self, v, info): # assert that v is basic if self.bascobas[v] >= self.n: - printout(info, "Cobasic variable", self.vartoa(v), - "should be basic") + self.printout(info, "Cobasic variable", self.vartoa(v), + "should be basic") exit(1) return def assertcobasic(self, v, info): # assert that v is cobasic if self.bascobas[v] < self.n: - printout(info, "Cobasic variable", self.vartoa(v), - "should be cobasic") + self.printout(info, "Cobasic variable", self.vartoa(v), + "should be cobasic") exit(1) return @@ -250,15 +244,15 @@ def docupivot(self, leave, enter): # leave, enter in VARS self.assertcobasic(enter, "docupivot") s = "leaving: " + self.vartoa(leave).ljust(5) s += "entering: " + self.vartoa(enter) - printout(s) + self.printout(s) return def raytermination(self, enter): - printout("Ray termination when trying to enter", self.vartoa(enter)) - printout(self) - printout("Current basis not an LCP solution:") + self.printout("Ray termination when trying to enter", self.vartoa(enter)) + self.printout(self) + self.printout("Current basis not an LCP solution:") self.createsol() - printout(self.outsol()) + self.printout(self.outsol()) exit(1) def testtablvars(self): # msg only if error, continue @@ -268,9 +262,9 @@ def testtablvars(self): # msg only if error, continue # injective suffices for j in range(2 * n + 1): if j == i: - printout("First problem for j=", j, ":") + self.printout("First problem for j=", j, ":") # printout (f"{j=} {self.bascobas[j]=} {self.whichvar[j]=}") - printout( + self.printout( f"j={j} self.bascobas[j]={self.bascobas[j]} " f"self.whichvar[j]={self.whichvar[j]}" ) @@ -279,7 +273,7 @@ def testtablvars(self): # msg only if error, continue def complement(self, v): # Z(i),W(i) are complements n = self.n if v == 0: - printout("Attempt to find complement of z0") + self.printout("Attempt to find complement of z0") exit(1) if v > n: return v - n @@ -309,7 +303,7 @@ def outstatistics(self): stats.sprint(str(x / 10.0)) else: stats.sprint("-") - printout(stats) + self.printout(stats) # returns leave,z0leave # leave = leaving variable in VARS, given by lexmin row, @@ -344,7 +338,7 @@ def lexminvar(self, enter): j = 0 # going through j = 0..n while len(leavecand) > 1: if j > n: # impossible, perturbed RHS should have full rank - printout("lex-minratio test failed") + self.printout("lex-minratio test failed") exit(1) self.lextested[j] += 1 self.lexcomparisons[j] += len(leavecand) @@ -435,16 +429,19 @@ def pivot(self, leave, enter): # end of pivot (leave, enter) + def printout(self, *s): + if self.output_stream is None: + return + print(*s, file=self.output_stream) + def runlemke( self, *, verbose=False, lexstats=False, z0=False, - silent=False, - lcpfilename, + output_stream=None, ): - global filehandle # z0: printout value of z0 # flags.maxcount = 0; # flags.bdocupivot = 1; @@ -454,16 +451,14 @@ def runlemke( # flags.binteract = 0; # flags.blexstats = 0; - outfile = lcpfilename + ".out" + self.output_stream = output_stream - if silent: - filehandle = open(outfile, "w") # noqa: SIM115 n = self.n self.pivotcount = 1 # check if d is ok - TBC # if (flags.binitabl) - printout("After filltableau:") - printout(self) + self.printout("After filltableau:") + self.printout(self) # z0 enters the basis to obtain lex-feasible solution enter = 0 @@ -472,38 +467,38 @@ def runlemke( self.negcol(n + 1) # if (flags.binitabl) if verbose: - printout("After negcol:") - printout(self) + self.printout("After negcol:") + self.printout(self) while True: # main loop of complementary pivoting self.testtablvars() if z0: # printout progress of z0 if self.bascobas[0] < n: # z0 is basic - printout( + self.printout( "step,z0=", self.pivotcount, self.A[self.bascobas[0]][n + 1] / self.determinant ) else: - printout("step,z0=", self.pivotcount, 0.0) + self.printout("step,z0=", self.pivotcount, 0.0) # if (flags.bdocupivot) self.docupivot(leave, enter) self.pivot(leave, enter) if z0leave: if z0: - printout("step,z0=", self.pivotcount + 1, 0.0) + self.printout("step,z0=", self.pivotcount + 1, 0.0) break if verbose: - printout(self) + self.printout(self) enter = self.complement(leave) leave, z0leave = self.lexminvar(enter) self.pivotcount += 1 # if (flags.binitabl) - printout("Final tableau:") - printout(self) + self.printout("Final tableau:") + self.printout(self) # if (flags.boutsol) self.createsol() - printout(self.outsol()) + self.printout(self.outsol()) if lexstats: self.outstatistics() # end of class tableau @@ -518,12 +513,6 @@ def runlemke( is_flag=True, help="Printout intermediate tableaus", ) -@click.option( - "-s", - "--silent", - is_flag=True, - help="Send output to '[LCPFILENAME].out'", -) @click.option( "--z0", is_flag=True, @@ -533,20 +522,20 @@ def runlemke( "lcpfilename", type=click.Path(exists=True, readable=True, file_okay=True, dir_okay=False), ) -def main(verbose, silent, z0, lcpfilename): +def main(verbose, z0, lcpfilename): """ Tool for solving linear complementarity problems using Lemke's algorithm. [LCPFILENAME] is the path to the input file """ - printout(f"verbose={verbose} lcpfilename={lcpfilename} silent={silent} z0={z0}") + print(f"verbose={verbose} lcpfilename={lcpfilename} z0={z0}") # printout (f"{verbose}= {lcpfilename}= {silent}= {z0}=") m = lcp(lcpfilename) - printout(m) - printout("==================================") + print(m) + print("==================================") tabl = tableau(m) - tabl.runlemke(verbose=verbose, z0=z0, silent=silent, lcpfilename=lcpfilename) + tabl.runlemke(verbose=verbose, z0=z0, output_stream=sys.stdout) if __name__ == "__main__": diff --git a/tests/test_lcp.py b/tests/test_lcp.py index 255b704..bc43f68 100644 --- a/tests/test_lcp.py +++ b/tests/test_lcp.py @@ -26,7 +26,7 @@ def lemke_solver(lcp_instance: lcp) -> list[Fr]: """Runs Lemke's algorithm on the given LCP and returns the solution.""" tabl = tableau(lcp_instance) - tabl.runlemke(verbose=False, z0=False, silent=False) + tabl.runlemke() return tabl.solution @@ -242,7 +242,7 @@ def test_with_lcp_conditions(test_case: LCPTestCase, subtests): @pytest.mark.parametrize("test_case", FAILURE_CASES) def test_failure(test_case: LCPTestCase): """ - Test the Lemke solver on LCPs that terminate on a secondary ray + Test the Lemke solver on LCPs that terminate on a secondary ray by verifying that it raises SystemExit with code 1. """ lcp_instance = test_case.factory() From 9698addb47b67badcc0430f67dcf3c5ac1af6e0c Mon Sep 17 00:00:00 2001 From: nataliemes Date: Fri, 29 May 2026 20:45:10 +0400 Subject: [PATCH 07/10] Remove upper bounds for dependencies --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 723915f..23f10e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,9 @@ build-backend = "setuptools.build_meta" name = "lemke" version = "0.0.1" dependencies = [ - "numpy>=2.2,<2.3", - "matplotlib>=3.10,<3.11", - "click>=8.1,<9", + "numpy>=2.2", + "matplotlib>=3.10", + "click>=8.1", ] [project.optional-dependencies] From bf78ff4a333994f997eed51e74062b2d861d149b Mon Sep 17 00:00:00 2001 From: nataliemes Date: Mon, 22 Jun 2026 22:45:48 +0400 Subject: [PATCH 08/10] Add tests --- tests/test_lemke.py | 164 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/test_lemke.py diff --git a/tests/test_lemke.py b/tests/test_lemke.py new file mode 100644 index 0000000..837d348 --- /dev/null +++ b/tests/test_lemke.py @@ -0,0 +1,164 @@ +from fractions import Fraction + +import pytest +from click.testing import CliRunner + +from lemke.lemke import ( + lcp, + main, + tableau, +) + + +# --- LCP ------------------------------------------------------- +@pytest.mark.parametrize("raw, expected", [ + ("1", Fraction(1)), + ("0.75", Fraction(3, 4)), + ("-1/3", Fraction(-1, 3)), + ("2/4", Fraction(1, 2)), +]) +def test_lcp_valid_file(tmp_path, raw, expected): + content = f"n= 1\nM= {raw}\nq= 1\nd= 1\n" + file_path = tmp_path / "lcp" + file_path.write_text(content) + + m = lcp(str(file_path)) + assert m.M[0][0] == expected + + +@pytest.mark.parametrize("content", [ + "M= 1 0 0 1 q= 1 1 d= 1 1\n", # missing n= + "n= 2\nM= 1 0 0 1\nq= 1 1\nd= 1\n", # wrong number of values + "n= 2\nM= 1 0 0 1\nq= 1 1\nx= 1 1\n", # X= instead of d= + "invalid content", +]) +def test_lcp_invalid_file(tmp_path, content): + file_path = tmp_path / "lcp" + file_path.write_text(content) + + with pytest.raises(SystemExit) as exc_info: + lcp(str(file_path)) + + assert exc_info.value.code == 1 + + +# --- TABLEAU INIT ---------------------------------------------- +def test_tableau_dimensions(): + n = 3 + t = tableau(lcp(n)) + assert t.n == n + assert len(t.A) == n + assert len(t.A[0]) == n + 2 + + +def test_tableau_initial_basis(): + n = 3 + t = tableau(lcp(n)) + for i in range(2 * n + 1): + if i <= n: + assert t.bascobas[i] >= n, f"Z({i}) should be cobasic" + else: + assert t.bascobas[i] < n, f"W({i}) should be basic" + + +def test_tableau_bascobas_whichvar_are_inverses(): + n = 3 + t = tableau(lcp(n)) + for i in range(2 * n + 1): + assert t.bascobas[t.whichvar[i]] == i + assert t.whichvar[t.bascobas[i]] == i + + +# --- VARTOA ---------------------------------------------------- +def test_vartoa(): + n = 3 + t = tableau(lcp(n)) + assert t.vartoa(0) == "z0" + assert t.vartoa(n + 1) == "w1" + assert t.vartoa(n) == f"z{n}" + assert t.vartoa(n + n) == f"w{n}" + + +# --- COMPLEMENT ------------------------------------------------ +@pytest.mark.parametrize("i", [1, 2, 3]) +def test_complement_pairs(i): + n = 3 + t = tableau(lcp(n)) + assert t.complement(i) == n + i + assert t.complement(n + i) == i + + +def test_complement_z0_fails(): + t = tableau(lcp(2)) + with pytest.raises(SystemExit): + t.complement(0) + + +# --- PIVOT ----------------------------------------------------- +def test_pivot_correctly_swaps_basis_variables(): + m = lcp(2) + m.M = [ + [Fraction(1), Fraction(0)], + [Fraction(0), Fraction(1)], + ] + m.q = [Fraction(-1), Fraction(-2)] + m.d = [Fraction(1), Fraction(1)] + + t = tableau(m) + + enter = 0 # z0 (cobasic in col 0) + leave = t.whichvar[0] # w1 (basic in row 0) + + t.pivot(leave, enter) + + assert t.bascobas[enter] < t.n # entering variable is basic + assert t.bascobas[leave] >= t.n # leaving variable is cobasic + + +# --- LEXMINVAR ------------------------------------------------- +def test_lexminvar_without_positive_entry(): + t = tableau(lcp(2)) + + t.A[0][0] = -1 + t.A[1][0] = -3 + + enter = 0 # z0, cobasic in col 0 + + with pytest.raises(SystemExit) as exc_info: + t.lexminvar(enter) + + assert exc_info.value.code == 1 + + +@pytest.mark.parametrize( + "row0, row1", + [ + pytest.param([-1, 0, 0, 0], [3, 0, 0, 0], id="single_candidate"), + pytest.param([2, 0, 0, 6], [2, 0, 0, 4], id="multiple_candidates"), + ], +) +def test_lexminvar_with_positive_entry(row0, row1): + t = tableau(lcp(2)) + t.A[0] = row0 + t.A[1] = row1 + + enter = 0 # z0, cobasic in col 0 + leave, z0leave = t.lexminvar(enter) + + assert leave == t.whichvar[1] # w2 leaves + assert z0leave is False + + +# --- CLI ------------------------------------------------------- +@pytest.mark.parametrize("extra_args", [ + [], + ["--verbose", "--z0"], +]) +def test_cli_runs_without_error(tmp_path, extra_args): + file_path = tmp_path / "test.lcp" + file_path.write_text("n= 2\nM= 1 0 0 1\nq= 1 1\nd= 1 1\n") + + runner = CliRunner() + result = runner.invoke(main, [str(file_path)] + extra_args) + + assert result.exit_code == 0 From 32dc4d80e705032a4f78b3626061083e1d155e62 Mon Sep 17 00:00:00 2001 From: nataliemes Date: Sat, 4 Jul 2026 19:23:51 +0400 Subject: [PATCH 09/10] Add CLI tests for invalid path inputs --- tests/test_lemke.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_lemke.py b/tests/test_lemke.py index 837d348..f52b73d 100644 --- a/tests/test_lemke.py +++ b/tests/test_lemke.py @@ -162,3 +162,21 @@ def test_cli_runs_without_error(tmp_path, extra_args): result = runner.invoke(main, [str(file_path)] + extra_args) assert result.exit_code == 0 + + +def test_cli_rejects_missing_file(tmp_path): + missing_path = tmp_path / "missing" + + runner = CliRunner() + result = runner.invoke(main, [str(missing_path)]) + + assert result.exit_code == 2 + assert "does not exist" in result.output.lower() + + +def test_cli_rejects_directory(tmp_path): + runner = CliRunner() + result = runner.invoke(main, [str(tmp_path)]) + + assert result.exit_code == 2 + assert "is a directory" in result.output.lower() From ab263d1425598426a6f8b35dd4f4c36166d9a8ee Mon Sep 17 00:00:00 2001 From: nataliemes Date: Sat, 4 Jul 2026 21:41:43 +0400 Subject: [PATCH 10/10] Show LCPFILENAME as a required argument in docstring --- src/lemke/lemke.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lemke/lemke.py b/src/lemke/lemke.py index 705913b..c65e042 100644 --- a/src/lemke/lemke.py +++ b/src/lemke/lemke.py @@ -526,7 +526,7 @@ def main(verbose, z0, lcpfilename): """ Tool for solving linear complementarity problems using Lemke's algorithm. - [LCPFILENAME] is the path to the input file + LCPFILENAME is the path to the input file. """ print(f"verbose={verbose} lcpfilename={lcpfilename} z0={z0}")