diff --git a/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs b/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs index 005b372..3cdd8ce 100644 --- a/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs +++ b/Coder.Test/Languages/PythonGeneratedSourceImportsTests.cs @@ -196,6 +196,58 @@ assert isinstance(shapes.Point.zero(), shapes.Point) }); } + /// + /// Tests that a class with a property that can be written but not read can be loaded and set. + /// + /// + /// @level.setter names the property a getter made, so a property with no getter needs + /// another spelling or the class statement raises NameError. + /// + [TestMethod] + public void WriteOnlyProperty_ImportsAndSets() + { + string? python = ToolchainHarness.FindOnPath("--version", "python3", "python"); + if (python is null) + { + Assert.Inconclusive("No Python interpreter on the path, so nothing was run."); + return; + } + + ClassDeclaration sink = new("Sink"); + PropertyDeclaration level = new("level", "int") { HasGetter = false, HasSetter = true }; + CallExpression record = new("record"); + record.Arguments.Add(new VariableReference("value")); + level.SetterBody.Add(new ExpressionStatement(record)); + sink.Members.Add(level); + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText(Path.Combine(directory, "sink.py"), new PythonGenerator().Generate(sink)); + File.WriteAllText( + Path.Combine(directory, "driver.py"), + """ + import runpy + + recorded = [] + module = runpy.run_path("sink.py", init_globals={"record": recorded.append}) + + instance = module["Sink"]() + instance.level = 3 + assert recorded == [3], recorded + + try: + instance.level + except AttributeError: + pass + else: + raise AssertionError("a write-only property could be read") + """); + + (int exitCode, string output) = ToolchainHarness.Run(python, "driver.py", directory); + Assert.AreEqual(0, exitCode, $"Python rejected the generated module:{Environment.NewLine}{output}"); + }); + } + /// /// Builds a file whose declarations are each written over themselves. /// diff --git a/Coder/Languages/PythonGenerator.cs b/Coder/Languages/PythonGenerator.cs index 55462d6..49dc9ca 100644 --- a/Coder/Languages/PythonGenerator.cs +++ b/Coder/Languages/PythonGenerator.cs @@ -131,6 +131,18 @@ protected override void GeneratePropertyDeclaration(PropertyDeclaration declarat WriteAccessorBody(declaration.GetterBody, code); } + if (declaration.CanWrite && !declaration.CanRead) + { + // `@name.setter` is an attribute of the property the getter made, so with no getter there + // is nothing for the decorator to name and the class statement raises NameError. The + // property is built from the setter alone instead. + string setter = $"_set_{name}"; + code.Write($"def {setter}(self, value{annotation}) -> None:"); + WriteAccessorBody(declaration.SetterBody, code); + code.WriteLine($"{name} = property(fset={setter})"); + return; + } + if (declaration.CanWrite) { code.WriteLine($"@{name}.setter");