diff --git a/TODO.org b/TODO.org index 1716ec28..8da01239 100644 --- a/TODO.org +++ b/TODO.org @@ -1,11 +1,14 @@ * untypy ** add tests with type annotations *** for records -**** construction **** write access +**** test for correct location +** understand why commit 150c7a5b209ddb264fd104feae42eba4905c2be7 is necessary to fix the replTester test * other +** mode where all functions or methods without type signatures are reported as errors ** upload dist to pypi ** update README +*** python version *** pip *** records *** import diff --git a/python/.ignore b/python/.ignore new file mode 100644 index 00000000..eed76414 --- /dev/null +++ b/python/.ignore @@ -0,0 +1,2 @@ +site-lib/wypp +site-lib/untypy diff --git a/python/deps/untypy b/python/deps/untypy index 5e58090c..16ce8157 160000 --- a/python/deps/untypy +++ b/python/deps/untypy @@ -1 +1 @@ -Subproject commit 5e58090c59b5a382a596e67718e64bc00bce3916 +Subproject commit 16ce81576c36bcd15b0db3d3147416921823f450 diff --git a/python/file-tests/testArgs.out b/python/file-tests/testArgs.out deleted file mode 100644 index 6840595f..00000000 --- a/python/file-tests/testArgs.out +++ /dev/null @@ -1 +0,0 @@ -['file-tests/testArgs.py', 'ARG_1', 'ARG_2'] diff --git a/python/file-tests/testTraceback.err b/python/file-tests/testTraceback.err deleted file mode 100644 index e20739f5..00000000 --- a/python/file-tests/testTraceback.err +++ /dev/null @@ -1,7 +0,0 @@ - -Traceback (most recent call last): - File "file-tests/testTraceback.py", line 9, in - foo(lst) - File "file-tests/testTraceback.py", line 7, in foo - print(lst[10]) -IndexError: list index out of range diff --git a/python/file-tests/testTraceback2.err b/python/file-tests/testTraceback2.err deleted file mode 100644 index 55342d7e..00000000 --- a/python/file-tests/testTraceback2.err +++ /dev/null @@ -1,5 +0,0 @@ - - File "file-tests/testTraceback2.py", line 3 - lst = [1,2,3 - ^ -SyntaxError: unexpected EOF while parsing diff --git a/python/file-tests/testTypes1.err b/python/file-tests/testTypes1.err deleted file mode 100644 index 86fffb7c..00000000 --- a/python/file-tests/testTypes1.err +++ /dev/null @@ -1,17 +0,0 @@ - -given: '1' -expected: int - ^^^ - -inside of inc(x: int) -> int - ^^^ -declared at: -file-tests/testTypes1.py:1 - 1 | def inc(x: int) -> int: - 2 | return x + 1 - - -caused by: -file-tests/testTypes1.py:4 - 4 | inc("1") - diff --git a/python/file-tests/testTypes2.err b/python/file-tests/testTypes2.err deleted file mode 100644 index d3bc73e6..00000000 --- a/python/file-tests/testTypes2.err +++ /dev/null @@ -1,17 +0,0 @@ - -given: '1' -expected: int - ^^^ - -inside of inc(x: int) -> int - ^^^ -declared at: -file-tests/testTypes2.py:1 - 1 | def inc(x: int) -> int: - 2 | return x - - -caused by: -file-tests/testTypes2.py:4 - 4 | inc("1") - diff --git a/python/integration-tests/shell.py b/python/integration-tests/shell.py index 7e573fc6..0d53673d 100644 --- a/python/integration-tests/shell.py +++ b/python/integration-tests/shell.py @@ -322,10 +322,12 @@ def hook(self): def exit(self, code=0): if code is None: - code = 0 + myCode = 0 elif type(code) != int: - code = 1 - self.exitCode = code + myCode = 1 + else: + myCode = code + self.exitCode = myCode self._origExit(code) def exc_handler(self, exc_type, exc, *args): diff --git a/python/integration-tests/testIntegration.py b/python/integration-tests/testIntegration.py index 51cf445a..d59cf9d4 100644 --- a/python/integration-tests/testIntegration.py +++ b/python/integration-tests/testIntegration.py @@ -32,67 +32,65 @@ def stripTrailingWs(s): LOG_FILE = shell.mkTempFile(prefix="wypp-tests", suffix=".log", deleteAtExit='ifSuccess') print(f'Output of integration tests goes to {LOG_FILE}') -LOG_REDIR = f'> {LOG_FILE} 2>&1' +LOG_REDIR = f'>> {LOG_FILE} 2>&1' class TypeTests(unittest.TestCase): def test_enumOk(self): - out = runInteractive('file-tests/typeEnums.py', 'colorToNumber("red")') + out = runInteractive('test-data/typeEnums.py', 'colorToNumber("red")') self.assertEqual(['0'], out) def test_enumTypeError(self): - out = runInteractive('file-tests/typeEnums.py', 'colorToNumber(1)')[0] - self.assertIn("expected: Literal['red', 'yellow', 'green']", out) + out = runInteractive('test-data/typeEnums.py', 'colorToNumber(1)')[0] + self.assertIn("expected: value of type Literal['red', 'yellow', 'green']", out) def test_recordOk(self): - rec = 'file-tests/typeRecords.py' + rec = 'test-data/typeRecords.py' out1 = runInteractive(rec, 'Person("stefan", 42)') self.assertEqual(["Person(name='stefan', age=42)"], out1) out2 = runInteractive(rec, 'incAge(Person("stefan", 42))') self.assertEqual(["Person(name='stefan', age=43)"], out2) - @unittest.skip def test_recordFail1(self): - rec = 'file-tests/typeRecords.py' + rec = 'test-data/typeRecords.py' out = runInteractive(rec, 'Person("stefan", 42.3)')[0] - self.assertIn('expected: int', out) + self.assertIn('expected: value of type int', out) def test_recordFail2(self): - rec = 'file-tests/typeRecords.py' + rec = 'test-data/typeRecords.py' out = runInteractive(rec, 'mutableIncAge(Person("stefan", 42))')[0] - self.assertIn('expected: MutablePerson', out) + self.assertIn('expected: value of type MutablePerson', out) def test_recordMutableOk(self): - rec = 'file-tests/typeRecords.py' + rec = 'test-data/typeRecords.py' out1 = runInteractive(rec, 'MutablePerson("stefan", 42)') self.assertEqual(["MutablePerson(name='stefan', age=42)"], out1) out2 = runInteractive(rec, 'p = MutablePerson("stefan", 42)\nmutableIncAge(p)\np') self.assertEqual(['', '', "MutablePerson(name='stefan', age=43)"], out2) - @unittest.skip def test_mutableRecordFail1(self): - rec = 'file-tests/typeRecords.py' + rec = 'test-data/typeRecords.py' out = runInteractive(rec, 'MutablePerson("stefan", 42.3)')[0] - self.assertIn('expected: int', out) + self.assertIn('expected: value of type int', out) def test_mutableRecordFail2(self): - rec = 'file-tests/typeRecords.py' + rec = 'test-data/typeRecords.py' out = runInteractive(rec, 'incAge(MutablePerson("stefan", 42))')[0] - self.assertIn('expected: Person', out) + self.assertIn('expected: value of type Person', out) @unittest.skip def test_mutableRecordFail3(self): - rec = 'file-tests/typeRecords.py' + rec = 'test-data/typeRecords.py' out = runInteractive(rec, 'p = MutablePerson("stefan", 42)\np.age = 42.4') - self.assertIn('expected: int', out) + self.assertIn('expected: value of type int', out) def test_union(self): - out = runInteractive('file-tests/typeUnion.py', """formatAnimal(myCat) + out = runInteractive('test-data/typeUnion.py', """formatAnimal(myCat) formatAnimal(myParrot) formatAnimal(None) """) self.assertEqual("'Cat Pumpernickel'", out[0]) self.assertEqual("\"Parrot Mike says: Let's go to the punkrock show\"", out[1]) - self.assertIn('given: None\nexpected: Union[Cat, Parrot]', out[2]) + self.assertIn('given: None\nexpected: value of type Union[Cat, Parrot]', out[2]) class StudentSubmissionTests(unittest.TestCase): def check(self, file, testFile, ecode, tycheck=True): @@ -100,72 +98,83 @@ def check(self, file, testFile, ecode, tycheck=True): if not tycheck: flags.append('--no-typechecking') cmd = f"python3 src/runYourProgram.py {' '.join(flags)} --test-file {testFile} {file} {LOG_REDIR}" - print(cmd) res = shell.run(cmd, onError='ignore') self.assertEqual(ecode, res.exitcode) def test_goodSubmission(self): - self.check("file-tests/student-submission.py", "file-tests/student-submission-tests.py", 0) - self.check("file-tests/student-submission.py", "file-tests/student-submission-tests.py", 0, + self.check("test-data/student-submission.py", "test-data/student-submission-tests.py", 0) + self.check("test-data/student-submission.py", "test-data/student-submission-tests.py", 0, tycheck=False) def test_badSubmission(self): - self.check("file-tests/student-submission-bad.py", - "file-tests/student-submission-tests.py", 1) - self.check("file-tests/student-submission-bad.py", - "file-tests/student-submission-tests.py", 1, tycheck=False) + self.check("test-data/student-submission-bad.py", + "test-data/student-submission-tests.py", 1) + self.check("test-data/student-submission-bad.py", + "test-data/student-submission-tests.py", 1, tycheck=False) def test_submissionWithTypeErrors(self): - self.check("file-tests/student-submission-tyerror.py", - "file-tests/student-submission-tests.py", 1) - self.check("file-tests/student-submission-tyerror.py", - "file-tests/student-submission-tests.py", 0, tycheck=False) - self.check("file-tests/student-submission.py", - "file-tests/student-submission-tests-tyerror.py", 1) - self.check("file-tests/student-submission.py", - "file-tests/student-submission-tests-tyerror.py", 0, tycheck=False) + self.check("test-data/student-submission-tyerror.py", + "test-data/student-submission-tests.py", 1) + self.check("test-data/student-submission-tyerror.py", + "test-data/student-submission-tests.py", 0, tycheck=False) + self.check("test-data/student-submission.py", + "test-data/student-submission-tests-tyerror.py", 1) + self.check("test-data/student-submission.py", + "test-data/student-submission-tests-tyerror.py", 0, tycheck=False) class InteractiveTests(unittest.TestCase): def test_scopeBugPeter(self): - out = runInteractive('file-tests/scope-bug-peter.py', 'local_test()\nprint(spam)') + out = runInteractive('test-data/scope-bug-peter.py', 'local_test()\nprint(spam)') self.assertIn('IT WORKS', out) def test_types1(self): - out = runInteractive('file-tests/testTypesInteractive.py', 'inc(3)') + out = runInteractive('test-data/testTypesInteractive.py', 'inc(3)') self.assertEqual(['4'], out) def test_types2(self): - out = runInteractive('file-tests/testTypesInteractive.py', 'inc("3")')[0] - expected = """given: '3' -expected: int - ^^^ + out = runInteractive('test-data/testTypesInteractive.py', 'inc("3")')[0] + expected = """untypy.error.UntypyTypeError +given: '3' +expected: value of type int + +context: inc(x: int) -> int + ^^^ +declared at: /Users/swehr/devel/write-your-python-program/python/test-data/testTypesInteractive.py:1 + 1 | def inc(x: int) -> int: + 2 | return x + 1 -inside of inc(x: int) -> int - ^^^ -declared at:""" - self.assertIn(expected, stripTrailingWs(out)) +caused by: :1""" + self.assertEqual(expected, out) def test_types3(self): - out = runInteractive('file-tests/testTypesInteractive.py', + out = runInteractive('test-data/testTypesInteractive.py', 'def f(x: int) -> int: return x\n\nf("x")')[1] - self.assertIn('expected: int', out) + self.assertIn('expected: value of type int', out) def test_types4(self): - out = runInteractive('file-tests/testTypesInteractive.py', + out = runInteractive('test-data/testTypesInteractive.py', 'def f(x: int) -> int: return x\n\nf(3)') self.assertEqual(['...', '3'], out) def test_types5(self): - out = runInteractive('file-tests/testTypesInteractive.py', + out = runInteractive('test-data/testTypesInteractive.py', 'def f(x: int) -> int: return x\n\nf("x")', tycheck=False) self.assertEqual(['...', "'x'"], out) def test_typesInImportedModule1(self): - out = run('file-tests/testTypes3.py', ecode=1) - self.assertIn('expected: int', out) + out = run('test-data/testTypes3.py', ecode=1) + self.assertIn('expected: value of type int', out) def test_typesInImportedModule2(self): - out = run('file-tests/testTypes3.py', tycheck=False) + out = run('test-data/testTypes3.py', tycheck=False) self.assertEqual('END', out) + +class ReplTesterTests(unittest.TestCase): + + def test_replTester(self): + d = shell.pwd() + cmd = f'python3 {d}/src/replTester.py {d}/test-data/repl-test-lib.py --repl {d}/test-data/repl-test-checks.py' + res = shell.run(cmd, captureStdout=True, onError='die', cwd='/tmp') + self.assertIn('All 1 tests succeded. Great!', res.stdout) diff --git a/python/run b/python/run new file mode 100755 index 00000000..20dcb450 --- /dev/null +++ b/python/run @@ -0,0 +1,5 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +PYTHONPATH="$SCRIPT_DIR"/site-lib/ python3 "$SCRIPT_DIR"/src/runYourProgram.py "$@" diff --git a/python/runFileTests.sh b/python/runFileTests.sh index 567097a9..e0368194 100755 --- a/python/runFileTests.sh +++ b/python/runFileTests.sh @@ -10,7 +10,6 @@ d=$(pwd) siteDir=$(python3 -c 'import site; print(site.USER_SITE)') t=$(mktemp) -echo echo "Running file tests, siteDir=$siteDir ..." echo "Writing logs to $t" function check() @@ -23,10 +22,10 @@ function check() python3 $d/src/runYourProgram.py --check --install-mode assertInstall $d/"$1" >> "$t" popd > /dev/null } -check file-tests/fileWithImport.py -check file-tests/fileWithoutImport.py -check file-tests/fileWithBothImports.py -check file-tests/fileWithRecursiveTypes.py +check test-data/fileWithImport.py +check test-data/fileWithoutImport.py +check test-data/fileWithBothImports.py +check test-data/fileWithRecursiveTypes.py # First argument: whether to do type checking or not # Second argument: expected exit code. If given as X:Y, then X is the exit code with active @@ -37,6 +36,7 @@ function checkWithOutputAux() local tycheck="$1" local expectedEcode=$2 local file="$3" + echo "Checking $file" shift 3 tycheckOpt="" suffixes="${PYENV_VERSION}" @@ -101,11 +101,34 @@ function checkWithOutput() checkWithOutputAux no "$@" } -checkWithOutput 1 file-tests/testTraceback.py -checkWithOutput 1 file-tests/testTraceback2.py -checkWithOutput 1 file-tests/testTraceback3.py -checkWithOutput 0 file-tests/testArgs.py ARG_1 ARG_2 -checkWithOutput 0 file-tests/printModuleName.py -checkWithOutput 0 file-tests/printModuleNameImport.py -checkWithOutput 1 file-tests/testTypes1.py -checkWithOutput 1:0 file-tests/testTypes2.py +checkWithOutput 1 test-data/testTraceback.py +checkWithOutput 1 test-data/testTraceback2.py +checkWithOutput 1 test-data/testTraceback3.py +checkWithOutput 0 test-data/testArgs.py ARG_1 ARG_2 +checkWithOutput 0 test-data/printModuleName.py +checkWithOutput 0 test-data/printModuleNameImport.py +checkWithOutput 1 test-data/testTypes1.py +checkWithOutput 1:0 test-data/testTypes2.py +checkWithOutputAux yes 1 test-data/testTypesCollections1.py +checkWithOutputAux yes 1 test-data/testTypesCollections2.py +# checkWithOutputAux yes 1 test-data/testTypesCollections3.py See #5 +# checkWithOutputAux yes 1 test-data/testTypesCollections4.py See #6 +checkWithOutputAux yes 1 test-data/testTypesProtos1.py +# checkWithOutputAux yes 1 test-data/testTypesProtos2.py See #8 +checkWithOutputAux yes 1 test-data/testTypesProtos3.py +# checkWithOutputAux yes 1 test-data/testTypesProtos4.py See #9 +# checkWithOutputAux yes 1 test-data/testTypesSubclassing1.py See #10 +# checkWithOutputAux yes 1 test-data/testTypesHigherOrderFuns.py See #7 +# checkWithOutputAux yes 1 test-data/testTypesRecordInheritance.py See #11 +checkWithOutputAux yes 0 test-data/testForwardRef1.py +# checkWithOutputAux yes 1 test-data/testForwardRef2.py See #14 +checkWithOutputAux yes 0 test-data/testForwardRef3.py +# checkWithOutputAux yes 1 test-data/testForwardRef4.py See #14 +# checkWithOutputAux yes 1 test-data/testTypesReturn.py See #15 +checkWithOutputAux yes 1 test-data/testTypesSequence1.py +checkWithOutputAux yes 1 test-data/testTypesSequence2.py +checkWithOutputAux yes 1 test-data/testTypesTuple1.py +# checkWithOutputAux yes 1 test-data/wrong-caused-by.py See #17 +# checkWithOutputAux yes 1 test-data/declared-aty.py See #18 + + diff --git a/python/runTestsForPyVersion.sh b/python/runTestsForPyVersion.sh index be275b8c..0af1708c 100755 --- a/python/runTestsForPyVersion.sh +++ b/python/runTestsForPyVersion.sh @@ -1,11 +1,20 @@ #!/bin/bash set -e +set -u cd $(dirname $0) unit_test_path=src:tests:deps/untypy -integ_test_path=integration-tests + +function prepare_integration_tests() +{ + echo "Preparing integration tests by install the WYPP library" + local d=$(mktemp -d) + trap "rm -rf $d" EXIT + WYPP_INSTALL_DIR=$d python3 src/runYourProgram.py --install-mode installOnly + integ_test_path=integration-tests:$d +} function usage() { @@ -13,11 +22,15 @@ function usage() exit 1 } -if [ -z "$1" ]; then - echo "Running all unit tests" +if [ -z "${1:-}" ]; then + echo "Running all unit tests, PYTHONPATH=$unit_test_path" PYTHONPATH=$unit_test_path python3 -m unittest tests/test*.py - echo "Running all integration tests" + echo "Done with unit tests" + echo + prepare_integration_tests + echo "Running all integration tests, PYTHONPATH=$integ_test_path" PYTHONPATH=$integ_test_path python3 -m unittest integration-tests/test*.py + echo "Done with integration tests" else if [ "$1" == "--unit" ]; then what="unit" @@ -26,13 +39,14 @@ else elif [ "$1" == "--integration" ]; then what="integration" dir=integration-tests + prepare_integration_tests p=$integ_test_path else usage fi shift - echo "Running $what tests $@" - if [ -z "$1" ]; then + echo "Running $what tests $@ with PYTHONPATH=$p" + if [ -z "${1:-}" ]; then PYTHONPATH=$p python3 -m unittest $dir/test*.py ecode=$? else diff --git a/python/site-lib/README b/python/site-lib/README new file mode 100644 index 00000000..d2769afc --- /dev/null +++ b/python/site-lib/README @@ -0,0 +1,3 @@ +This directory contains symlinks for wypp and untypy. + +Add this directory to PYTHONPATH if you want to import wypp and untypy in-place. diff --git a/python/site-lib/untypy b/python/site-lib/untypy new file mode 120000 index 00000000..db337ec2 --- /dev/null +++ b/python/site-lib/untypy @@ -0,0 +1 @@ +../deps/untypy/untypy/ \ No newline at end of file diff --git a/python/site-lib/wypp b/python/site-lib/wypp new file mode 120000 index 00000000..e057607e --- /dev/null +++ b/python/site-lib/wypp @@ -0,0 +1 @@ +../src/ \ No newline at end of file diff --git a/python/src/__init__.py b/python/src/__init__.py index c866c517..887dfe6d 100644 --- a/python/src/__init__.py +++ b/python/src/__init__.py @@ -3,7 +3,6 @@ # Exported names that are available for star imports (in alphabetic order) Any = w.Any Callable = w.Callable -ForwardRef = w.ForwardRef Generator = w.Generator Iterable = w.Iterable Iterator = w.Iterator @@ -17,12 +16,14 @@ math = w.math nat = w.nat record = w.record +T = w.T +U = w.U unchecked = w.unchecked +V = w.V __all__ = [ 'Any', 'Callable', - 'ForwardRef', 'Generator', 'Iterable', 'Iterator', @@ -36,7 +37,10 @@ 'math', 'nat', 'record', - 'unchecked' + 'T', + 'U', + 'unchecked', + 'V' ] # Exported names not available for star imports (in alphabetic order) diff --git a/python/src/replTester.py b/python/src/replTester.py new file mode 100644 index 00000000..5f7e4ca7 --- /dev/null +++ b/python/src/replTester.py @@ -0,0 +1,101 @@ +import sys +import doctest +import os +import argparse +from dataclasses import dataclass +from runner import runCode, importUntypy, verbose, enableVerbose + +usage = """python3 replTester.py [ ARGUMENTS ] LIB_1 ... LIB_n --repl SAMPLE_1 ... SAMPLE_m + +If no library files should be used to test the REPL samples, omit LIB_1 ... LIB_n +and the --repl flag. +The definitions of LIB_1 ... LIB_n are made available when testing +SAMPLE_1 ... SAMPLE_m, where identifer in LIB_i takes precedence over identifier in +LIB_j if i > j. +""" + +@dataclass +class Options: + verbose: bool + diffOutput: bool + libs: list[str] + repls: list[str] + +def parseCmdlineArgs(): + parser = argparse.ArgumentParser(usage=usage, + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('--verbose', dest='verbose', action='store_const', + const=True, default=False, + help='Be verbose') + parser.add_argument('--diffOutput', dest='diffOutput', + action='store_const', const=True, default=False, + help='print diff of expected/given output') + args, restArgs = parser.parse_known_args() + libs = [] + repls = [] + replFlag = '--repl' + if replFlag in restArgs: + cur = libs + for x in restArgs: + if x == replFlag: + cur = repls + else: + cur.append(x) + else: + repls = restArgs + if len(repls) == 0: + print('No SAMPLE arguments given') + sys.exit(1) + return Options(verbose=args.verbose, diffOutput=args.diffOutput, libs=libs, repls=repls) + +opts = parseCmdlineArgs() + +if opts.verbose: + enableVerbose() + +libDir = os.path.dirname(__file__) +libFile = os.path.join(libDir, 'writeYourProgram.py') +defs = globals() +importUntypy() +# runCode(libFile, defs, []) + +for lib in opts.libs: + d = os.path.dirname(lib) + if d not in sys.path: + sys.path.insert(0, d) + +for lib in opts.libs: + verbose(f"Loading lib {lib}") + runCode(lib, defs, []) + +totalFailures = 0 +totalTests = 0 + +doctestOptions = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + +if opts.diffOutput: + doctestOptions = doctestOptions | doctest.REPORT_NDIFF + +for repl in opts.repls: + (failures, tests) = doctest.testfile(repl, globs=defs, module_relative=False, + optionflags=doctestOptions) + + totalFailures += failures + totalTests += tests + if failures == 0: + if tests == 0: + print(f'No tests in {repl}') + else: + print(f'All {tests} tests in {repl} succeeded') + else: + print(f'ERROR: {failures} out of {tests} in {repl} failed') + +if totalFailures == 0: + if totalTests == 0: + print('ERROR: No tests found at all!') + sys.exit(1) + else: + print(f'All {totalTests} tests succeded. Great!') +else: + print(f'ERROR: {failures} out of {tests} failed') + sys.exit(1) diff --git a/python/src/runner.py b/python/src/runner.py index 081af5ee..86f04310 100644 --- a/python/src/runner.py +++ b/python/src/runner.py @@ -24,6 +24,10 @@ def die(ecode=1): VERBOSE = False # set via commandline +def enableVerbose(): + global VERBOSE + VERBOSE = True + LIB_DIR = os.path.dirname(__file__) INSTALLED_MODULE_NAME = 'wypp' FILES_TO_INSTALL = ['writeYourProgram.py', 'drawingLib.py', '__init__.py'] @@ -125,13 +129,12 @@ def isSameFile(f1, f2): y = readFile(f2) return x == y -def installFromDir(srcDir, mod, files=None): +def installFromDir(srcDir, targetDir, mod, files=None): if files is None: files = [p.relative_to(srcDir) for p in Path(srcDir).rglob('*.py')] else: files = [Path(f) for f in files] - userDir = site.USER_SITE - installDir = os.path.join(userDir, mod) + installDir = os.path.join(targetDir, mod) os.makedirs(installDir, exist_ok=True) installedFiles = sorted([p.relative_to(installDir) for p in Path(installDir).rglob('*.py')]) wantedFiles = sorted(files) @@ -144,7 +147,7 @@ def installFromDir(srcDir, mod, files=None): break else: # no break, all files equal - verbose(f'All files from {srcDir} already installed in {userDir}/{mod}') + verbose(f'All files from {srcDir} already installed in {targetDir}/{mod}') return True else: verbose(f'Installed files {installedFiles} and wanted files {wantedFiles} are different') @@ -164,17 +167,17 @@ def installLib(mode): if mode == InstallMode.dontInstall: verbose("No installation of WYPP should be performed") return - userDir = site.USER_SITE + targetDir = os.getenv('WYPP_INSTALL_DIR', site.USER_SITE) try: - allEq1 = installFromDir(LIB_DIR, INSTALLED_MODULE_NAME, FILES_TO_INSTALL) - allEq2 = installFromDir(UNTYPY_DIR, UNTYPY_MODULE_NAME) + allEq1 = installFromDir(LIB_DIR, targetDir, INSTALLED_MODULE_NAME, FILES_TO_INSTALL) + allEq2 = installFromDir(UNTYPY_DIR, targetDir, UNTYPY_MODULE_NAME) if allEq1 and allEq2: - verbose(f'WYPP library in {userDir} already up to date') + verbose(f'WYPP library in {targetDir} already up to date') if mode == InstallMode.installOnly: - printStderr(f'WYPP library in {userDir} already up to date') + printStderr(f'WYPP library in {targetDir} already up to date') return else: - printStderr(f'The WYPP library has been successfully installed in {userDir}.') + printStderr(f'The WYPP library has been successfully installed in {targetDir}.') except Exception as e: printStderr('Installation of the WYPP library failed: ' + str(e)) if mode == InstallMode.assertInstall or mode == InstallMode.installOnly: @@ -201,7 +204,7 @@ def __init__(self, mod, properlyImported): if name and name[0] != '_': d[name] = getattr(mod, name) -def loadLib(onlyCheckRunnable): +def prepareLib(onlyCheckRunnable): libDefs = None mod = INSTALLED_MODULE_NAME verbose('Attempting to import ' + mod) @@ -259,7 +262,7 @@ def __exit__(self, exc_type, value, traceback): sys.path.remove(self.dir) self.inserted = False -def runCode(fileToRun, globals, args, *, useUntypy=True): +def runCode(fileToRun, globals, args, useUntypy=True): localDir = os.path.dirname(fileToRun) with sysPathPrepended(localDir): with open(fileToRun) as f: @@ -286,11 +289,7 @@ def runCode(fileToRun, globals, args, *, useUntypy=True): finally: sys.argv = oldArgs -def runStudentCode(fileToRun, globals, libDefs, onlyCheckRunnable, args, *, useUntypy=True): - importsWypp = findWyppImport(fileToRun) - if importsWypp: - if not libDefs.properlyImported: - globals[INSTALLED_MODULE_NAME] = libDefs.dict +def runStudentCode(fileToRun, globals, onlyCheckRunnable, args, useUntypy=True): doRun = lambda: runCode(fileToRun, globals, args, useUntypy=useUntypy) if onlyCheckRunnable: try: @@ -361,7 +360,11 @@ def limitTraceback(fullTb): def handleCurrentException(exit=True, removeFirstTb=False, file=sys.stderr): (etype, val, tb) = sys.exc_info() if isinstance(val, untypy.error.UntypyTypeError) or isinstance(val, untypy.error.UntypyAttributeError): - file.write(str(val)) + file.write(etype.__module__ + "." + etype.__qualname__) + s = str(val) + if s and s[0] != '\n': + file.write(': ') + file.write(s) file.write('\n') else: if tb and removeFirstTb: @@ -384,6 +387,11 @@ def getHistoryFilePath(): else: return None +# We cannot import untypy at the top of the file because we might have to install it first. +def importUntypy(): + global untypy + import untypy + def main(globals): v = sys.version_info if v.major < 3 or v.minor < 9: @@ -398,9 +406,7 @@ def main(globals): VERBOSE = True installLib(args.installMode) - - global untypy - import untypy + importUntypy() fileToRun = args.file if args.changeDir: @@ -415,13 +421,13 @@ def main(globals): if not args.checkRunnable and not args.quiet: printWelcomeString(fileToRun, version, useUntypy=args.checkTypes) - libDefs = loadLib(onlyCheckRunnable=args.checkRunnable) + libDefs = prepareLib(onlyCheckRunnable=args.checkRunnable) globals['__name__'] = '__wypp__' sys.modules['__wypp__'] = sys.modules['__main__'] try: verbose(f'running code in {fileToRun}') - runStudentCode(fileToRun, globals, libDefs, args.checkRunnable, restArgs, + runStudentCode(fileToRun, globals, args.checkRunnable, restArgs, useUntypy=args.checkTypes) except: handleCurrentException() diff --git a/python/src/testReplSamples.py b/python/src/testReplSamples.py deleted file mode 100644 index bbc7d557..00000000 --- a/python/src/testReplSamples.py +++ /dev/null @@ -1,74 +0,0 @@ -import sys -import doctest -import os -from runner import runCode - -def usage(): - print('USAGE: python3 testReplSamples.py LIB_1 ... LIB_n --repl SAMPLE_1 ... SAMPLE_m') - print('If no library files should be used to test the REPL samples, omit LIB_1 ... LIB_n') - print('and the --repl flag.') - print('The definitions of LIB_1 ... LIB_n are made available when testing ') - print('SAMPLE_1 ... SAMPLE_m, where identifer in LIB_i takes precedence over identifier in ') - print('LIB_j if i > j.') - sys.exit(1) - -args = sys.argv[1:] - -if '--help' in args: - usage() - -libs = [] -repls = [] - -replFlag = '--repl' - -if replFlag in args: - cur = libs - for x in args: - if x == replFlag: - cur = repls - else: - cur.append(x) -else: - repls = args - -if len(repls) == 0: - usage() - -libDir = os.path.dirname(__file__) -libFile = os.path.join(libDir, 'writeYourProgram.py') -defs = {} -runCode(libFile, defs, []) - -for lib in libs: - d = os.path.dirname(lib) - if d not in sys.path: - sys.path.insert(0, d) - -for lib in libs: - runCode(lib, defs, []) - -totalFailures = 0 -totalTests = 0 - -for repl in repls: - (failures, tests) = doctest.testfile(repl, globs=defs, module_relative=False) - totalFailures += failures - totalTests += tests - if failures == 0: - if tests == 0: - print(f'No tests in {repl}') - else: - print(f'All {tests} tests in {repl} succeeded') - else: - print(f'ERROR: {failures} out of {tests} in {repl} failed') - -if totalFailures == 0: - if totalTests == 0: - print('ERROR: No tests found at all!') - sys.exit(1) - else: - print(f'All {totalTests} tests succeded. Great!') -else: - print(f'ERROR: {failures} out of {tests} failed') - sys.exit(1) diff --git a/python/src/writeYourProgram.py b/python/src/writeYourProgram.py index 1f279943..c0d66e65 100644 --- a/python/src/writeYourProgram.py +++ b/python/src/writeYourProgram.py @@ -26,14 +26,17 @@ def _debug(s): dataclass = dataclasses.dataclass -# Reexports for Untypy unchecked = untypy.unchecked nat = typing.Annotated[int, lambda i: i >= 0] +T = typing.TypeVar('T') +U = typing.TypeVar('U') +V = typing.TypeVar('V') + def _patchDataClass(cls, mutable): fieldNames = [f.name for f in dataclasses.fields(cls)] setattr(cls, EQ_ATTRS_ATTR, fieldNames) - + if hasattr(cls, '__annotations__'): # add annotions for type checked constructor. cls.__init__.__annotations__ = cls.__annotations__ diff --git a/python/test-data/declared-at-missing.py b/python/test-data/declared-at-missing.py new file mode 100644 index 00000000..979ff573 --- /dev/null +++ b/python/test-data/declared-at-missing.py @@ -0,0 +1,22 @@ +from wypp import * + +@record +class Course: + name: str + teacher: str + students: tuple[str, ...] + +@record(mutable=True) +class CourseM: + name: str + teacher: str + students: tuple[str, ...] + +@record +class Semester: + degreeProgram: str + semester: str + courses: tuple[CourseM, ...] + +prog1 = Course('Programmierung 1', 'Wehr', ()) +semester1_2020 = Semester('AKI', '1. Semester 2020/21', (prog1, )) diff --git a/python/file-tests/fileWithBothImports.py b/python/test-data/fileWithBothImports.py similarity index 90% rename from python/file-tests/fileWithBothImports.py rename to python/test-data/fileWithBothImports.py index 29df392d..489c85e4 100644 --- a/python/file-tests/fileWithBothImports.py +++ b/python/test-data/fileWithBothImports.py @@ -11,4 +11,3 @@ def use(x): use(wypp.record) use(drawingLib.Point) use(Point) -use(wypp.list[int]) diff --git a/python/file-tests/fileWithImport.py b/python/test-data/fileWithImport.py similarity index 79% rename from python/file-tests/fileWithImport.py rename to python/test-data/fileWithImport.py index e8b11c9b..861fdb88 100644 --- a/python/file-tests/fileWithImport.py +++ b/python/test-data/fileWithImport.py @@ -6,4 +6,3 @@ def use(x): use(wypp) use(wypp.record) -use(wypp.list[int]) diff --git a/python/file-tests/fileWithRecursiveTypes.py b/python/test-data/fileWithRecursiveTypes.py similarity index 100% rename from python/file-tests/fileWithRecursiveTypes.py rename to python/test-data/fileWithRecursiveTypes.py diff --git a/python/file-tests/fileWithStarImport.py b/python/test-data/fileWithStarImport.py similarity index 100% rename from python/file-tests/fileWithStarImport.py rename to python/test-data/fileWithStarImport.py diff --git a/python/file-tests/fileWithoutImport.py b/python/test-data/fileWithoutImport.py similarity index 100% rename from python/file-tests/fileWithoutImport.py rename to python/test-data/fileWithoutImport.py diff --git a/python/file-tests/localMod.py b/python/test-data/localMod.py similarity index 100% rename from python/file-tests/localMod.py rename to python/test-data/localMod.py diff --git a/python/file-tests/printModuleName.err b/python/test-data/printModuleName.err similarity index 100% rename from python/file-tests/printModuleName.err rename to python/test-data/printModuleName.err diff --git a/python/file-tests/printModuleName.out b/python/test-data/printModuleName.out similarity index 100% rename from python/file-tests/printModuleName.out rename to python/test-data/printModuleName.out diff --git a/python/file-tests/printModuleName.py b/python/test-data/printModuleName.py similarity index 100% rename from python/file-tests/printModuleName.py rename to python/test-data/printModuleName.py diff --git a/python/file-tests/printModuleNameImport.err b/python/test-data/printModuleNameImport.err similarity index 100% rename from python/file-tests/printModuleNameImport.err rename to python/test-data/printModuleNameImport.err diff --git a/python/file-tests/printModuleNameImport.out b/python/test-data/printModuleNameImport.out similarity index 100% rename from python/file-tests/printModuleNameImport.out rename to python/test-data/printModuleNameImport.out diff --git a/python/file-tests/printModuleNameImport.py b/python/test-data/printModuleNameImport.py similarity index 100% rename from python/file-tests/printModuleNameImport.py rename to python/test-data/printModuleNameImport.py diff --git a/python/test-data/repl-test-checks.py b/python/test-data/repl-test-checks.py new file mode 100644 index 00000000..8f2560c2 --- /dev/null +++ b/python/test-data/repl-test-checks.py @@ -0,0 +1,2 @@ +>>> dora.gewicht +25000 diff --git a/python/test-data/repl-test-lib.py b/python/test-data/repl-test-lib.py new file mode 100644 index 00000000..4b836670 --- /dev/null +++ b/python/test-data/repl-test-lib.py @@ -0,0 +1,10 @@ +from wypp import * + +Status = Literal['tot', 'lebendig'] + +@record +class Gürteltier: + gewicht: int + totOderLebendig: Status + +dora = Gürteltier(25000, 'lebendig') diff --git a/python/file-tests/scope-bug-peter.py b/python/test-data/scope-bug-peter.py similarity index 100% rename from python/file-tests/scope-bug-peter.py rename to python/test-data/scope-bug-peter.py diff --git a/python/file-tests/student-submission-bad.py b/python/test-data/student-submission-bad.py similarity index 100% rename from python/file-tests/student-submission-bad.py rename to python/test-data/student-submission-bad.py diff --git a/python/file-tests/student-submission-tests-tyerror.py b/python/test-data/student-submission-tests-tyerror.py similarity index 100% rename from python/file-tests/student-submission-tests-tyerror.py rename to python/test-data/student-submission-tests-tyerror.py diff --git a/python/file-tests/student-submission-tests.py b/python/test-data/student-submission-tests.py similarity index 100% rename from python/file-tests/student-submission-tests.py rename to python/test-data/student-submission-tests.py diff --git a/python/file-tests/student-submission-tyerror.py b/python/test-data/student-submission-tyerror.py similarity index 100% rename from python/file-tests/student-submission-tyerror.py rename to python/test-data/student-submission-tyerror.py diff --git a/python/file-tests/student-submission.py b/python/test-data/student-submission.py similarity index 100% rename from python/file-tests/student-submission.py rename to python/test-data/student-submission.py diff --git a/python/file-tests/testArgs.err b/python/test-data/testArgs.err similarity index 100% rename from python/file-tests/testArgs.err rename to python/test-data/testArgs.err diff --git a/python/test-data/testArgs.out b/python/test-data/testArgs.out new file mode 100644 index 00000000..982a2823 --- /dev/null +++ b/python/test-data/testArgs.out @@ -0,0 +1 @@ +['test-data/testArgs.py', 'ARG_1', 'ARG_2'] diff --git a/python/file-tests/testArgs.py b/python/test-data/testArgs.py similarity index 100% rename from python/file-tests/testArgs.py rename to python/test-data/testArgs.py diff --git a/python/file-tests/testTraceback2.out b/python/test-data/testForwardRef1.err similarity index 100% rename from python/file-tests/testTraceback2.out rename to python/test-data/testForwardRef1.err diff --git a/python/test-data/testForwardRef1.out b/python/test-data/testForwardRef1.out new file mode 100644 index 00000000..345e6aef --- /dev/null +++ b/python/test-data/testForwardRef1.out @@ -0,0 +1 @@ +Test diff --git a/python/test-data/testForwardRef1.py b/python/test-data/testForwardRef1.py new file mode 100644 index 00000000..9aed46b3 --- /dev/null +++ b/python/test-data/testForwardRef1.py @@ -0,0 +1,11 @@ +class Test: + def __init__(self, foo: 'Foo'): + pass + def __repr__(self): + return 'Test' + +class Foo: + pass + +t = Test(Foo()) +print(t) diff --git a/python/test-data/testForwardRef2.py b/python/test-data/testForwardRef2.py new file mode 100644 index 00000000..40523896 --- /dev/null +++ b/python/test-data/testForwardRef2.py @@ -0,0 +1,11 @@ +class Test: + def __init__(self, foo: 'Foo'): + pass + def __repr__(self): + return 'Test' + +class FooX: + pass + +t = Test(FooX()) +print(t) diff --git a/python/file-tests/testTraceback3.out b/python/test-data/testForwardRef3.err similarity index 100% rename from python/file-tests/testTraceback3.out rename to python/test-data/testForwardRef3.err diff --git a/python/test-data/testForwardRef3.out b/python/test-data/testForwardRef3.out new file mode 100644 index 00000000..82cf2102 --- /dev/null +++ b/python/test-data/testForwardRef3.out @@ -0,0 +1 @@ +Test(foo=Foo) diff --git a/python/test-data/testForwardRef3.py b/python/test-data/testForwardRef3.py new file mode 100644 index 00000000..d0e3759f --- /dev/null +++ b/python/test-data/testForwardRef3.py @@ -0,0 +1,12 @@ +from wypp import * + +@record +class Test: + foo: 'Foo' + +class Foo: + def __repr__(self): + return 'Foo' + +t = Test(Foo()) +print(t) diff --git a/python/test-data/testForwardRef4.py b/python/test-data/testForwardRef4.py new file mode 100644 index 00000000..2cb7d5ff --- /dev/null +++ b/python/test-data/testForwardRef4.py @@ -0,0 +1,12 @@ +from wypp import * + +@record +class Test: + foo: 'FooX' + +class Foo: + def __repr__(self): + return 'Foo' + +t = Test(Foo()) +print(t) diff --git a/python/test-data/testTraceback.err b/python/test-data/testTraceback.err new file mode 100644 index 00000000..d12f2238 --- /dev/null +++ b/python/test-data/testTraceback.err @@ -0,0 +1,7 @@ + +Traceback (most recent call last): + File "test-data/testTraceback.py", line 9, in + foo(lst) + File "test-data/testTraceback.py", line 7, in foo + print(lst[10]) +IndexError: list index out of range diff --git a/python/file-tests/testTraceback.out b/python/test-data/testTraceback.out similarity index 100% rename from python/file-tests/testTraceback.out rename to python/test-data/testTraceback.out diff --git a/python/file-tests/testTraceback.py b/python/test-data/testTraceback.py similarity index 100% rename from python/file-tests/testTraceback.py rename to python/test-data/testTraceback.py diff --git a/python/file-tests/testTraceback2.err-3.9.0 b/python/test-data/testTraceback2.err similarity index 62% rename from python/file-tests/testTraceback2.err-3.9.0 rename to python/test-data/testTraceback2.err index e3f643b9..281b1771 100644 --- a/python/file-tests/testTraceback2.err-3.9.0 +++ b/python/test-data/testTraceback2.err @@ -1,5 +1,5 @@ - File "file-tests/testTraceback2.py", line 3 + File "test-data/testTraceback2.py", line 3 lst = [1,2,3 ^ SyntaxError: unexpected EOF while parsing diff --git a/python/file-tests/testTypes1.out b/python/test-data/testTraceback2.out similarity index 100% rename from python/file-tests/testTypes1.out rename to python/test-data/testTraceback2.out diff --git a/python/file-tests/testTraceback2.py b/python/test-data/testTraceback2.py similarity index 100% rename from python/file-tests/testTraceback2.py rename to python/test-data/testTraceback2.py diff --git a/python/file-tests/testTraceback3.err b/python/test-data/testTraceback3.err similarity index 61% rename from python/file-tests/testTraceback3.err rename to python/test-data/testTraceback3.err index e2de6b58..c59940d1 100644 --- a/python/file-tests/testTraceback3.err +++ b/python/test-data/testTraceback3.err @@ -1,5 +1,5 @@ Traceback (most recent call last): - File "file-tests/testTraceback3.py", line 2, in + File "test-data/testTraceback3.py", line 2, in print([1,2,3][10]) IndexError: list index out of range diff --git a/python/file-tests/testTypes2.err-notypes b/python/test-data/testTraceback3.out similarity index 100% rename from python/file-tests/testTypes2.err-notypes rename to python/test-data/testTraceback3.out diff --git a/python/file-tests/testTraceback3.py b/python/test-data/testTraceback3.py similarity index 100% rename from python/file-tests/testTraceback3.py rename to python/test-data/testTraceback3.py diff --git a/python/test-data/testTypes1.err b/python/test-data/testTypes1.err new file mode 100644 index 00000000..d52cba13 --- /dev/null +++ b/python/test-data/testTypes1.err @@ -0,0 +1,12 @@ +untypy.error.UntypyTypeError +given: '1' +expected: value of type int + +context: inc(x: int) -> int + ^^^ +declared at: test-data/testTypes1.py:1 + 1 | def inc(x: int) -> int: + 2 | return x + 1 + +caused by: test-data/testTypes1.py:4 + 4 | inc("1") diff --git a/python/file-tests/testTypes1.err-notypes b/python/test-data/testTypes1.err-notypes similarity index 53% rename from python/file-tests/testTypes1.err-notypes rename to python/test-data/testTypes1.err-notypes index ecbea461..c1014f7a 100644 --- a/python/file-tests/testTypes1.err-notypes +++ b/python/test-data/testTypes1.err-notypes @@ -1,7 +1,7 @@ Traceback (most recent call last): - File "file-tests/testTypes1.py", line 4, in + File "test-data/testTypes1.py", line 4, in inc("1") - File "file-tests/testTypes1.py", line 2, in inc + File "test-data/testTypes1.py", line 2, in inc return x + 1 TypeError: can only concatenate str (not "int") to str diff --git a/python/file-tests/testTypes2.out b/python/test-data/testTypes1.out similarity index 100% rename from python/file-tests/testTypes2.out rename to python/test-data/testTypes1.out diff --git a/python/file-tests/testTypes1.py b/python/test-data/testTypes1.py similarity index 100% rename from python/file-tests/testTypes1.py rename to python/test-data/testTypes1.py diff --git a/python/test-data/testTypes2.err b/python/test-data/testTypes2.err new file mode 100644 index 00000000..6a5fed85 --- /dev/null +++ b/python/test-data/testTypes2.err @@ -0,0 +1,12 @@ +untypy.error.UntypyTypeError +given: '1' +expected: value of type int + +context: inc(x: int) -> int + ^^^ +declared at: test-data/testTypes2.py:1 + 1 | def inc(x: int) -> int: + 2 | return x + +caused by: test-data/testTypes2.py:4 + 4 | inc("1") diff --git a/python/test-data/testTypes2.err-notypes b/python/test-data/testTypes2.err-notypes new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypes2.out b/python/test-data/testTypes2.out new file mode 100644 index 00000000..e69de29b diff --git a/python/file-tests/testTypes2.py b/python/test-data/testTypes2.py similarity index 100% rename from python/file-tests/testTypes2.py rename to python/test-data/testTypes2.py diff --git a/python/file-tests/testTypes3.py b/python/test-data/testTypes3.py similarity index 100% rename from python/file-tests/testTypes3.py rename to python/test-data/testTypes3.py diff --git a/python/test-data/testTypesCollections1.err b/python/test-data/testTypesCollections1.err new file mode 100644 index 00000000..2606513a --- /dev/null +++ b/python/test-data/testTypesCollections1.err @@ -0,0 +1,12 @@ +untypy.error.UntypyTypeError +given: 'foo' +expected: value of type int + +context: list[int] + ^^^ +declared at: test-data/testTypesCollections1.py:3 + 3 | def appendSomething(l: list[int]) -> None: + 4 | l.append("foo") + +caused by: test-data/testTypesCollections1.py:4 + 4 | l.append("foo") diff --git a/python/test-data/testTypesCollections1.out b/python/test-data/testTypesCollections1.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesCollections1.py b/python/test-data/testTypesCollections1.py new file mode 100644 index 00000000..be569e10 --- /dev/null +++ b/python/test-data/testTypesCollections1.py @@ -0,0 +1,8 @@ +from wypp import * + +def appendSomething(l: list[int]) -> None: + l.append("foo") + +l = [1,2,3] +appendSomething(l) +print(l) diff --git a/python/test-data/testTypesCollections2.err b/python/test-data/testTypesCollections2.err new file mode 100644 index 00000000..1f7b2b48 --- /dev/null +++ b/python/test-data/testTypesCollections2.err @@ -0,0 +1,15 @@ +untypy.error.UntypyTypeError +given: 42 +expected: value of type str + +context: foo(l: list[Callable[[], str]]) -> list[str] + ^^^ +declared at: test-data/testTypesCollections2.py:3 + 3 | def foo(l: list[Callable[[], str]]) -> list[str]: + 4 | res = [] + 5 | for f in l: + 6 | res.append(f()) + 7 | return res + +caused by: test-data/testTypesCollections2.py:10 + 10 | foo([lambda: "1", lambda: 42]) # error because the 2nd functions returns an int diff --git a/python/test-data/testTypesCollections2.out b/python/test-data/testTypesCollections2.out new file mode 100644 index 00000000..189744a2 --- /dev/null +++ b/python/test-data/testTypesCollections2.out @@ -0,0 +1 @@ +['1', '2'] diff --git a/python/test-data/testTypesCollections2.py b/python/test-data/testTypesCollections2.py new file mode 100644 index 00000000..4e9c4997 --- /dev/null +++ b/python/test-data/testTypesCollections2.py @@ -0,0 +1,10 @@ +from wypp import * + +def foo(l: list[Callable[[], str]]) -> list[str]: + res = [] + for f in l: + res.append(f()) + return res + +print(foo([lambda: "1", lambda: "2"])) +foo([lambda: "1", lambda: 42]) # error because the 2nd functions returns an int diff --git a/python/test-data/testTypesCollections3.py b/python/test-data/testTypesCollections3.py new file mode 100644 index 00000000..0bd390ec --- /dev/null +++ b/python/test-data/testTypesCollections3.py @@ -0,0 +1,8 @@ +from wypp import * + +def appendSomething(l: list[int], l2: list[int]) -> None: + l.append("foo") + +l = [1,2,3] +appendSomething(l, []) +print(l) diff --git a/python/test-data/testTypesCollections4.py b/python/test-data/testTypesCollections4.py new file mode 100644 index 00000000..8ca810bc --- /dev/null +++ b/python/test-data/testTypesCollections4.py @@ -0,0 +1,10 @@ +from wypp import * + +def foo(l: list[Callable[[], str]]) -> list[str]: + l.append(lambda: 42) # error + res = [] + for f in l: + res.append(f()) + return res + +foo([]) diff --git a/python/test-data/testTypesHigherOrderFuns.py b/python/test-data/testTypesHigherOrderFuns.py new file mode 100644 index 00000000..68a4abe5 --- /dev/null +++ b/python/test-data/testTypesHigherOrderFuns.py @@ -0,0 +1,10 @@ +from wypp import * + +def map(container: Iterable[str], fun: Callable[[str], int]) -> list[int]: + res = [] + for x in container: + res.append(fun(x)) + return res + +print(map(["hello", "1"], len)) +map(["hello", "1"], lambda x: x) diff --git a/python/file-tests/testTypesInteractive.py b/python/test-data/testTypesInteractive.py similarity index 100% rename from python/file-tests/testTypesInteractive.py rename to python/test-data/testTypesInteractive.py diff --git a/python/test-data/testTypesProtos1.err b/python/test-data/testTypesProtos1.err new file mode 100644 index 00000000..e38d02ae --- /dev/null +++ b/python/test-data/testTypesProtos1.err @@ -0,0 +1,34 @@ +untypy.error.UntypyTypeError +Type 'Dog' does not implement protocol 'Animal' correctly. + +given: +expected: value of type Animal + +context: doSomething(a: Animal) -> None + ^^^^^^ +declared at: test-data/testTypesProtos1.py:18 + 18 | def doSomething(a: Animal) -> None: + 19 | print(a.makeSound(3.14)) + +caused by: test-data/testTypesProtos1.py:21 + 21 | doSomething(Dog()) + +The argument 'loadness' of method 'makeSound' violates the protocol 'Animal'. +The annotation 'int' is incompatible with the protocol's annotation 'float' +when checking against the following value: + +given: 3.14 +expected: value of type int + +context: makeSound(self: Self, loadness: int) -> str + ^^^ +declared at: test-data/testTypesProtos1.py:13 + 13 | def makeSound(self, loadness: int) -> str: + 14 | return f"{loadness} wuffs" +test-data/testTypesProtos1.py:8 + 8 | def makeSound(self, loadness: float) -> str: + 9 | pass + +caused by: test-data/testTypesProtos1.py:13 + 13 | def makeSound(self, loadness: int) -> str: + 14 | return f"{loadness} wuffs" diff --git a/python/test-data/testTypesProtos1.out b/python/test-data/testTypesProtos1.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos1.py b/python/test-data/testTypesProtos1.py new file mode 100644 index 00000000..167878bb --- /dev/null +++ b/python/test-data/testTypesProtos1.py @@ -0,0 +1,21 @@ +from wypp import * + +from typing import Protocol +import abc + +class Animal(Protocol): + @abc.abstractmethod + def makeSound(self, loadness: float) -> str: + pass + +class Dog: + # incorrect implementation of the Animal protocol + def makeSound(self, loadness: int) -> str: + return f"{loadness} wuffs" + def __repr__(self): + return "" + +def doSomething(a: Animal) -> None: + print(a.makeSound(3.14)) + +doSomething(Dog()) diff --git a/python/test-data/testTypesProtos2.py b/python/test-data/testTypesProtos2.py new file mode 100644 index 00000000..fa2c6e3d --- /dev/null +++ b/python/test-data/testTypesProtos2.py @@ -0,0 +1,19 @@ +from wypp import * + +from typing import Protocol +import abc + +class Animal(Protocol): + @abc.abstractmethod + def makeSound(loadness: float) -> str: # self parameter omitted! + pass + +class Dog: + # incorrect implementation of the Animal protocol + def makeSound(self, loadness: int) -> str: + return f"{loadness} wuffs" + +def doSomething(a: Animal) -> None: + print(a.makeSound(3.14)) + +doSomething(Dog()) diff --git a/python/test-data/testTypesProtos3.err b/python/test-data/testTypesProtos3.err new file mode 100644 index 00000000..4be46a5f --- /dev/null +++ b/python/test-data/testTypesProtos3.err @@ -0,0 +1,14 @@ +untypy.error.UntypyTypeError +Type Dog does not meet the requirements of protocol Animal. The signature of 'makeSound' does not match. Missing required parameter self + +given: 'Dog' +expected: value of type Animal + +context: doSomething(a: Animal) -> None + ^^^^^^ +declared at: test-data/testTypesProtos3.py:16 + 16 | def doSomething(a: Animal) -> None: + 17 | print(a.makeSound(3.14)) + +caused by: test-data/testTypesProtos3.py:19 + 19 | doSomething(Dog()) diff --git a/python/test-data/testTypesProtos3.out b/python/test-data/testTypesProtos3.out new file mode 100644 index 00000000..e69de29b diff --git a/python/test-data/testTypesProtos3.py b/python/test-data/testTypesProtos3.py new file mode 100644 index 00000000..334c2424 --- /dev/null +++ b/python/test-data/testTypesProtos3.py @@ -0,0 +1,19 @@ +from wypp import * + +from typing import Protocol +import abc + +class Animal(Protocol): + @abc.abstractmethod + def makeSound(self, loadness: float) -> str: + pass + +class Dog: + # incorrect implementation of the Animal protocol + def makeSound(loadness: int) -> str: # self parameter omitted! + return f"{loadness} wuffs" + +def doSomething(a: Animal) -> None: + print(a.makeSound(3.14)) + +doSomething(Dog()) diff --git a/python/test-data/testTypesProtos4.py b/python/test-data/testTypesProtos4.py new file mode 100644 index 00000000..5ce3a127 --- /dev/null +++ b/python/test-data/testTypesProtos4.py @@ -0,0 +1,27 @@ +from wypp import * + +from typing import Protocol +import abc + +class Interface(Protocol): + abc.abstractmethod + def meth(self) -> Callable[[int], int]: + pass + +class ConcreteCorrect: + def meth(self) -> Callable[[int], int]: + return lambda x: x + 1 + +def bar(s: str) -> int: + return len(s) + +class ConcreteWrong: + def meth(self) -> Callable[[int], int]: + return lambda x: bar(x) # invalid call of bar with argument of type int + +def foo(obj: Interface) -> int: + fn = obj.meth() + return fn(2) + +print(foo(ConcreteCorrect())) +print(foo(ConcreteWrong())) diff --git a/python/test-data/testTypesRecordInheritance.py b/python/test-data/testTypesRecordInheritance.py new file mode 100644 index 00000000..6dfd7037 --- /dev/null +++ b/python/test-data/testTypesRecordInheritance.py @@ -0,0 +1,14 @@ +from wypp import * + +@record +class Point2D: + x: int + y: int + +@record +class Point3D(Point2D): + z: int + +print(Point3D(1,2,3)) +Point3D(1,2, "foo") + diff --git a/python/test-data/testTypesReturn.py b/python/test-data/testTypesReturn.py new file mode 100644 index 00000000..f1aaa969 --- /dev/null +++ b/python/test-data/testTypesReturn.py @@ -0,0 +1,8 @@ +def foo(flag: bool) -> int: + print('Hello World') + if flag: + return 1 + else: + return 'you stupid' + +foo(False) diff --git a/python/test-data/testTypesSequence1.err b/python/test-data/testTypesSequence1.err new file mode 100644 index 00000000..eb475241 --- /dev/null +++ b/python/test-data/testTypesSequence1.err @@ -0,0 +1,13 @@ +untypy.error.UntypyTypeError +given: 1 +expected: value of type Sequence + +context: foo(seq: Sequence) -> None + ^^^^^^^^ +declared at: test-data/testTypesSequence1.py:3 + 3 | def foo(seq: Sequence) -> None: + 4 | print(seq) + 5 | pass + +caused by: test-data/testTypesSequence1.py:10 + 10 | foo(1) # should fail diff --git a/python/test-data/testTypesSequence1.out b/python/test-data/testTypesSequence1.out new file mode 100644 index 00000000..f4ce9837 --- /dev/null +++ b/python/test-data/testTypesSequence1.out @@ -0,0 +1,3 @@ +[1, 2, 3] +('bar', 'baz') +Hello! diff --git a/python/test-data/testTypesSequence1.py b/python/test-data/testTypesSequence1.py new file mode 100644 index 00000000..8a34e873 --- /dev/null +++ b/python/test-data/testTypesSequence1.py @@ -0,0 +1,10 @@ +from wypp import * + +def foo(seq: Sequence) -> None: + print(seq) + pass + +foo([1,2,3]) +foo( ("bar", "baz") ) +foo("Hello!") +foo(1) # should fail diff --git a/python/test-data/testTypesSequence2.err b/python/test-data/testTypesSequence2.err new file mode 100644 index 00000000..abccc73a --- /dev/null +++ b/python/test-data/testTypesSequence2.err @@ -0,0 +1,13 @@ +untypy.error.UntypyTypeError +given: 'Hello!' +expected: value of type Sequence[int] + +context: foo(seq: Sequence[int]) -> None + ^^^^^^^^^^^^^ +declared at: test-data/testTypesSequence2.py:3 + 3 | def foo(seq: Sequence[int]) -> None: + 4 | print(seq) + 5 | pass + +caused by: test-data/testTypesSequence2.py:9 + 9 | foo("Hello!") # should fail diff --git a/python/test-data/testTypesSequence2.out b/python/test-data/testTypesSequence2.out new file mode 100644 index 00000000..ec306cac --- /dev/null +++ b/python/test-data/testTypesSequence2.out @@ -0,0 +1,2 @@ +[1, 2, 3] +(4, 5) diff --git a/python/test-data/testTypesSequence2.py b/python/test-data/testTypesSequence2.py new file mode 100644 index 00000000..290dee38 --- /dev/null +++ b/python/test-data/testTypesSequence2.py @@ -0,0 +1,9 @@ +from wypp import * + +def foo(seq: Sequence[int]) -> None: + print(seq) + pass + +foo([1,2,3]) +foo( (4,5) ) +foo("Hello!") # should fail diff --git a/python/test-data/testTypesSubclassing1.py b/python/test-data/testTypesSubclassing1.py new file mode 100644 index 00000000..3bba042c --- /dev/null +++ b/python/test-data/testTypesSubclassing1.py @@ -0,0 +1,25 @@ +from wypp import * + +class AnimalFood: + def __init__(self, name: str): + self.name = name + +class Animal: + def feed(self, food: AnimalFood) -> None: + print(f'Tasty animal food: {food.name}') + +class DogFood(AnimalFood): + def __init__(self, name: str, weight: float): + super().__init__(name) + self.weight = weight + +class Dog(Animal): + # Dog provides an invalid override for feed + def feed(self, food: DogFood) -> None: + print(f'Tasty dog food: {food.name} ({food.weight}g)') + +def feedAnimal(a: Animal) -> None: + a.feed(AnimalFood('some cat food')) + +dog = Dog() +feedAnimal(dog) diff --git a/python/test-data/testTypesTuple1.err b/python/test-data/testTypesTuple1.err new file mode 100644 index 00000000..c9ae74fb --- /dev/null +++ b/python/test-data/testTypesTuple1.err @@ -0,0 +1,12 @@ +untypy.error.UntypyTypeError +given: 1 +expected: value of type tuple[int, ...] + +context: foo(l: tuple[int, ...]) -> int + ^^^^^^^^^^^^^^^ +declared at: test-data/testTypesTuple1.py:1 + 1 | def foo(l: tuple[int, ...]) -> int: + 2 | return len(l) + +caused by: test-data/testTypesTuple1.py:5 + 5 | foo(1) diff --git a/python/test-data/testTypesTuple1.out b/python/test-data/testTypesTuple1.out new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/python/test-data/testTypesTuple1.out @@ -0,0 +1 @@ +3 diff --git a/python/test-data/testTypesTuple1.py b/python/test-data/testTypesTuple1.py new file mode 100644 index 00000000..29ea2c3c --- /dev/null +++ b/python/test-data/testTypesTuple1.py @@ -0,0 +1,5 @@ +def foo(l: tuple[int, ...]) -> int: + return len(l) + +print(foo((1,2,3))) +foo(1) diff --git a/python/file-tests/typeEnums.py b/python/test-data/typeEnums.py similarity index 100% rename from python/file-tests/typeEnums.py rename to python/test-data/typeEnums.py diff --git a/python/file-tests/typeRecords.py b/python/test-data/typeRecords.py similarity index 100% rename from python/file-tests/typeRecords.py rename to python/test-data/typeRecords.py diff --git a/python/file-tests/typeUnion.py b/python/test-data/typeUnion.py similarity index 100% rename from python/file-tests/typeUnion.py rename to python/test-data/typeUnion.py diff --git a/python/test-data/wrong-caused-by.py b/python/test-data/wrong-caused-by.py new file mode 100644 index 00000000..d7ca90e2 --- /dev/null +++ b/python/test-data/wrong-caused-by.py @@ -0,0 +1,30 @@ +from wypp import * + +@record(mutable=True) +class StreetM: + name: str + cars: list[CarM] + # Einbiegen eines Auto auf eine Straße + # SEITENEFFEKT: Liste der Autos wird geändert. + def turnIntoStreet(self: StreetM, car: Car) -> None: + if car not in self.cars: + self.cars.append(car) + # Verlassen einer Straße + # SEITENEFFEKT: Liste der Autos wird geändert. + def leaveStreet(self: StreetM, car: Car) -> None: + if car in self.cars: + self.cars.remove(car) + +@record(mutable=True) +class CarM: + licensePlate: str + color: str + +@record +class Car: + licensePlate: str + color: str + +redCarM = CarM('OG PY 123', 'rot') +mainStreetM = StreetM('Hauptstraße', [redCarM]) +mainStreetM.turnIntoStreet(redCarM) diff --git a/python/tests/sample.py b/python/tests/sample.py new file mode 100644 index 00000000..f020c989 --- /dev/null +++ b/python/tests/sample.py @@ -0,0 +1,124 @@ +from writeYourProgram import * +import math +from untypy import typechecked +import typing + +Drink = Literal["Tea", "Coffee"] + +# berechnet wieviele Tassen ich von einem Getränk trinken darf +@typechecked +def canDrink(d: Drink) -> int: + if d == "Tea": + return 5 + elif d == "Coffee": + return 1 + +# A shape is one of the following: +# - a circle (Circle) +# - a square (Square) +# - an overlay of two shapes (Overlay) +Shape = typing.Union[typing.ForwardRef('Circle'), typing.ForwardRef('Square'), typing.ForwardRef('Overlay')] +Shape = typing.Union['Circle', 'Square', 'Overlay'] + +# A point consists of +# - x (float) +# - y (float) +@record +class Point: + x: float + y: float +# Point: (float, float) -> Point +# For some Point p +# p.x: float +# p.y: float + +# point at x=10, y=20 +p1 = Point(10, 20) + +# point at x=30, y=50 +p2 = Point(30, 50) + +# point at x=40, y=30 +p3 = Point(40, 30) + +# A circle consists of +# - center (Point) +# - radius (float) +@record +class Circle: + center: Point + radius: float + +# Circle: (Point, float) -> Circle +# For some circle c +# c.center: Point +# c.radius: float + +# circle at p2 with radius=20 +c1 = Circle(p2, 20) + +# circle at p3 with radius=15 +c2 = Circle(p3, 15) + +# A square (parallel to the coordinate system) consists of +# - lower-left corner (Point) +# - size (float) +@record +class Square: + corner: Point + size: float + +# square at p1 with size=40 +s1 = Square(p1, 40) + +# Square: (Point, float) -> Square +# For some square s +# s.corner: Point +# s.size: float + +# An overlay consists of +# - top (Shape) +# - bottom (Shape) +@record +class Overlay: + top: Shape + bottom: Shape + +# Overlay: (Shape, Shape) -> Overlay +# For some overlay: +# o.top: Shape +# o.bottom: Shape + +# overlay of circle c1 and square s1 +o1 = Overlay(c1, s1) +# Overlay of overlay o1 and circle c2 +o2 = Overlay(o1, c2) + +# Calculate the distance between two points +@typechecked +def distance(p1: Point, p2: Point) -> float: + w = p1.x - p2.x + h = p1.y - p2.y + dist = math.sqrt(w**2 + h**2) + return dist + +# Is a point within a shape? +@typechecked +def pointInShape(point: Point, shape: Shape) -> bool: + px = point.x + py = point.y + if type(shape) == Circle: + return distance(point, shape.center) <= shape.radius + elif type(shape) == Square: + corner = shape.corner + size = shape.size + return ( + px >= corner.x and + px <= corner.x + size and + py >= corner.y and + py <= corner.y + size + ) + elif type(shape) == Overlay: + return pointInShape(point, shape.top) or pointInShape(point, shape.bottom) + else: + uncoveredCase() diff --git a/python/tests/testDeepEq.py b/python/tests/testDeepEq.py index 412b73f5..3883c1b9 100644 --- a/python/tests/testDeepEq.py +++ b/python/tests/testDeepEq.py @@ -1,5 +1,5 @@ import unittest -import testSample +import sample from writeYourProgram import * from writeYourProgram import deepEq import writeYourProgram as wypp @@ -67,27 +67,27 @@ def test_eq(self): self.assertFalse(deepEq(C(42.0), D(42.0000000000001), structuralObjEq=False, floatEqWithDelta=True)) self.assertTrue(deepEq( - testSample.Point(1.0, 2.0), testSample.Point(1.00000000001, 2), + sample.Point(1.0, 2.0), sample.Point(1.00000000001, 2), structuralObjEq=True, floatEqWithDelta=True )) self.assertFalse(deepEq( - testSample.Point(1.0, 2.0), testSample.Point(1.00000000001, 2), + sample.Point(1.0, 2.0), sample.Point(1.00000000001, 2), structuralObjEq=False, floatEqWithDelta=True )) self.assertTrue(deepEq( - Point(1.0, 2.0), testSample.Point(1.0, 2.0), + Point(1.0, 2.0), sample.Point(1.0, 2.0), structuralObjEq=True, floatEqWithDelta=True) ) self.assertFalse(deepEq( - Point(1.0, 2.0), testSample.Point(1.0, 2.0), + Point(1.0, 2.0), sample.Point(1.0, 2.0), structuralObjEq=False, floatEqWithDelta=True) ) self.assertFalse(deepEq( - testSample.Point(1.0, 3.0), testSample.Point(1.00000000001, 2), + sample.Point(1.0, 3.0), sample.Point(1.00000000001, 2), structuralObjEq=True, floatEqWithDelta=True )) self.assertFalse(deepEq( - testSample.Point(1.0, 3.0), testSample.Point(1.00000000001, 2), + sample.Point(1.0, 3.0), sample.Point(1.00000000001, 2), structuralObjEq=False, floatEqWithDelta=True )) self.assertTrue(deepEq(A(2), A(2), structuralObjEq=True, floatEqWithDelta=True)) diff --git a/python/tests/testSample.py b/python/tests/testSample.py index 1c248d32..65ab52b0 100644 --- a/python/tests/testSample.py +++ b/python/tests/testSample.py @@ -1,125 +1,9 @@ from writeYourProgram import * import unittest -import math +from sample import * setDieOnCheckFailures(True) -Drink = Literal["Tea", "Coffee"] - -# berechnet wieviele Tassen ich von einem Getränk trinken darf -def canDrink(d: Drink) -> int: - if d == "Tea": - return 5 - elif d == "Coffee": - return 1 - -# A shape is one of the following: -# - a circle (Circle) -# - a square (Square) -# - an overlay of two shapes (Overlay) -Shape = Union[ForwardRef('Circle'), ForwardRef('Square'), ForwardRef('Overlay')] - -# A point consists of -# - x (float) -# - y (float) -@record -class Point: - x: float - y: float -# Point: (float, float) -> Point -# For some Point p -# p.x: float -# p.y: float - -# point at x=10, y=20 -p1 = Point(10, 20) - -# point at x=30, y=50 -p2 = Point(30, 50) - -# point at x=40, y=30 -p3 = Point(40, 30) - -# A circle consists of -# - center (Point) -# - radius (float) -@record -class Circle: - center: Point - radius: float - -# Circle: (Point, float) -> Circle -# For some circle c -# c.center: Point -# c.radius: float - -# circle at p2 with radius=20 -c1 = Circle(p2, 20) - -# circle at p3 with radius=15 -c2 = Circle(p3, 15) - -# A square (parallel to the coordinate system) consists of -# - lower-left corner (Point) -# - size (float) -@record -class Square: - corner: Point - size: float - -# square at p1 with size=40 -s1 = Square(p1, 40) - -# Square: (Point, float) -> Square -# For some square s -# s.corner: Point -# s.size: float - -# An overlay consists of -# - top (Shape) -# - bottom (Shape) -@record -class Overlay: - top: Shape - bottom: Shape - -# Overlay: (Shape, Shape) -> Overlay -# For some overlay: -# o.top: Shape -# o.bottom: Shape - -# overlay of circle c1 and square s1 -o1 = Overlay(c1, s1) -# Overlay of overlay o1 and circle c2 -o2 = Overlay(o1, c2) - -# Calculate the distance between two points -def distance(p1: Point, p2: Point) -> float: - w = p1.x - p2.x - h = p1.y - p2.y - dist = math.sqrt(w**2 + h**2) - return dist - -# Is a point within a shape? -def pointInShape(point: Point, shape: Shape) -> bool: - px = point.x - py = point.y - if type(shape) == Circle: - return distance(point, shape.center) <= shape.radius - elif type(shape) == Square: - corner = shape.corner - size = shape.size - return ( - px >= corner.x and - px <= corner.x + size and - py >= corner.y and - py <= corner.y + size - ) - elif type(shape) == Overlay: - return pointInShape(point, shape.top) or pointInShape(point, shape.bottom) - else: - uncoveredCase() - class TestSample(unittest.TestCase): def test_sample(self): check(pointInShape(p2, c1), True)