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");