From c8253a6ba01e6e620b55811369028f14d301341d Mon Sep 17 00:00:00 2001 From: Janusch Jacoby Date: Tue, 15 Sep 2015 16:55:16 +0200 Subject: [PATCH 01/21] Manually merge changes from webpython branch. --- Gemfile | 2 + app/assets/javascripts/editor.js | 197 ++++++++++++++++++- app/assets/javascripts/turtle.js | 222 ++++++++++++++++++++++ app/controllers/submissions_controller.rb | 65 +++++-- app/views/exercises/implement.html.slim | 26 ++- lib/docker_client.rb | 93 ++++++++- 6 files changed, 576 insertions(+), 29 deletions(-) create mode 100644 app/assets/javascripts/turtle.js diff --git a/Gemfile b/Gemfile index 5ad9cd1d..3b1b2cd2 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,8 @@ gem 'thread_safe' gem 'turbolinks' gem 'uglifier', '>= 1.3.0' gem 'will_paginate', '~> 3.0' +gem 'tubesock' +gem 'faye-websocket' group :development do gem 'better_errors', platform: :ruby diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index e2598eb7..5186e3d0 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -20,6 +20,14 @@ $(function() { var qa_api = undefined; var output_mode_is_streaming = true; + var websocket, + turtlescreen, + numMessages = 0, + turtlecanvas = $('#turtlecanvas'), + prompt = $('#prompt'), + commands = ['input', 'write', 'turtle', 'turtlebatch'], + streams = ['stdin', 'stdout', 'stderr']; + var flowrResultHtml = '
' var ajax = function(options) { @@ -181,7 +189,13 @@ $(function() { }; var evaluateCodeWithStreamedResponse = function(url, callback) { - var event_source = new EventSource(url); + initWebsocketConnection(url); + + // TODO only init turtle when required + initTurtle(); + + // TODO reimplement via websocket messsages + /*var event_source = new EventSource(url); event_source.addEventListener('close', closeEventSource); event_source.addEventListener('error', closeEventSource); @@ -201,7 +215,7 @@ $(function() { event_source.addEventListener('status', function(event) { showStatus(JSON.parse(event.data)); - }); + });*/ }; var handleStreamedResponseForCodePilot = function(event) { @@ -428,7 +442,7 @@ $(function() { handleSidebarClick(e); }); */ - + //session session.on('annotationRemoval', handleAnnotationRemoval); session.on('annotationChange', handleAnnotationChange); @@ -1065,6 +1079,183 @@ $(function() { $('#request-for-comments').toggle(isActiveFileSubmission() && !isActiveFileBinary()); }; + var initWebsocketConnection = function(url) { + websocket = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + url); + websocket.onopen = function(evt) { onWebSocketOpen(evt) }; + websocket.onclose = function(evt) { onWebSocketClose(evt) }; + websocket.onmessage = function(evt) { onWebSocketMessage(evt) }; + websocket.onerror = function(evt) { onWebSocketError(evt) }; + websocket.flush = function() { this.send('\n'); } + }; + + var initTurtle = function() { + turtlescreen = new Turtle(websocket, $('#turtlecanvas')); + }; + + var initCallbacks = function() { + if ($('#run').isPresent()) { + $('#run').bind('click', function(event) { + hideCanvas(); + hidePrompt(); + }); + } + if ($('#prompt').isPresent()) { + $('#prompt').on('keypress', handlePromptKeyPress); + $('#prompt-submit').on('click', submitPromptInput); + } + } + + var onWebSocketOpen = function(evt) { + //alert("Session started"); + }; + + var onWebSocketClose = function(evt) { + //alert("Session terminated"); + }; + + var onWebSocketMessage = function(evt) { + numMessages++; + parseCanvasMessage(evt.data, true); + }; + + var onWebSocketError = function(evt) { + //alert("Something went wrong.") + }; + + var executeCommand = function(msg) { + if ($.inArray(msg.cmd, commands) == -1) { + console.log("Unknown command: " + msg.cmd); + // skipping unregistered commands is required + // as we may receive mirrored response due to internal behaviour + return; + } + switch(msg.cmd) { + case 'input': + showPrompt(); + break; + case 'write': + printWebsocketOutput(msg); + break; + case 'turtle': + showCanvas(); + handleTurtleCommand(msg); + break; + case 'turtlebatch': + showCanvas(); + handleTurtlebatchCommand(msg); + break; + } + }; + + // todo reuse method from editor.js + var printWebsocketOutput = function(msg) { + var element = findOrCreateOutputElement(0); + console.log(element); + switch (msg.stream) { + case 'internal': + element.addClass('text-danger'); + break; + case 'stderr': + element.addClass('text-warning'); + break; + case 'stdout': + case 'stdin': // for eventual prompts + default: + element.addClass('text-muted'); + } + element.append(msg.data) + }; + + var handleTurtleCommand = function(msg) { + if (msg.action in turtlescreen) { + result = turtlescreen[msg.action].apply(turtlescreen, msg.args); + websocket.send(JSON.stringify({cmd: 'result', 'result': result})); + } else { + websocket.send(JSON.stringify({cmd: 'exception', exception: 'AttributeError', message: msg.action})); + } + websocket.flush(); + }; + + var handleTurtlebatchCommand = function(msg) { + for (i = 0; i < msg.batch.length; i++) { + cmd = msg.batch[i]; + turtlescreen[cmd[0]].apply(turtlescreen, cmd[1]); + } + }; + + var handlePromptKeyPress = function(evt) { + if (evt.which === ENTER_KEY_CODE) { + submitPromptInput(); + } + } + + var submitPromptInput = function() { + var input = $('#prompt-input'); + var message = input.val(); + websocket.send(JSON.stringify({cmd: 'result', 'data': message})); + websocket.flush(); + input.val(''); + hidePrompt(); + } + + var parseCanvasMessage = function(message, recursive) { + var msg; + message = message.replace(/^\s+|\s+$/g, ""); + try { + // todo validate json instead of catching + msg = JSON.parse(message); + } catch (e) { + if (!recursive) { + return false; + } + // why does docker sometimes send multiple commands at once? + message = message.replace(/^\s+|\s+$/g, ""); + messages = message.split("\n"); + for (var i = 0; i < messages.length; i++) { + if (!messages[i]) { + continue; + } + parseCanvasMessage(messages[i], false); + } + return; + } + executeCommand(msg); + }; + + var showPrompt = function() { + if (prompt.isPresent() && prompt.hasClass('hidden')) { + prompt.removeClass('hidden'); + } + prompt.focus(); + } + + var hidePrompt = function() { + if (prompt.isPresent() && !prompt.hasClass('hidden')) { + console.log("hiding prompt2"); + prompt.addClass('hidden'); + } + } + + var showCanvas = function() { + if ($('#turtlediv').isPresent() + && turtlecanvas.hasClass('hidden')) { + // initialize two-column layout + $('#output-col1').addClass('col-lg-7 col-md-7 two-column'); + turtlecanvas.removeClass('hidden'); + } + }; + + var hideCanvas = function() { + if ($('#turtlediv').isPresent() + && !(turtlecanvas.hasClass('hidden'))) { + output = $('#output-col1'); + if (output.hasClass('two-column')) { + output.removeClass('col-lg-7 col-md-7 two-column'); + } + turtlecanvas.addClass('hidden'); + } + }; + var requestComments = function(e) { var user_id = $('#editor').data('user-id') var exercise_id = $('#editor').data('exercise-id') diff --git a/app/assets/javascripts/turtle.js b/app/assets/javascripts/turtle.js new file mode 100644 index 00000000..8aa8a9fe --- /dev/null +++ b/app/assets/javascripts/turtle.js @@ -0,0 +1,222 @@ +var output; +var editor; +var pipeurl; +var filename; +var pendingChanges = -1; + +function Turtle(pipe, canvas) { + var dx, dy, xpos, ypos; + this.canvas = canvas; // jQuery object + this.items = []; + this.canvas.off('click'); + this.canvas.click(function (e) { + if (e.eventPhase !== 2) { + return; + } + e.stopPropagation(); + dx = this.width / 2; + dy = this.height / 2; + if(e.offsetX==undefined) + { + var offset = canvas.offset(); + xpos = e.pageX-offset.left; + ypos = e.pageY-offset.top; + } + else + { + xpos = e.offsetX; + ypos = e.offsetY; + } + pipe.send(JSON.stringify({ + 'cmd': 'canvasevent', + 'type': '', + 'x': xpos - dx, + 'y': ypos - dy + })); + }); +} + +Turtle.prototype.update = function () { + var i, k, canvas, ctx, dx, dy, item, c, length; + canvas = this.canvas[0]; + ctx = canvas.getContext('2d'); + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + length = this.items.length; + dx = canvas.width / 2; + dy = canvas.height / 2; + for (i = 0; i < length; i += 1) { + item = this.items[i]; + c = item.coords; + switch (item.type) { + case 'line': + ctx.beginPath(); + ctx.moveTo(c[0] + dx, c[1] + dy); + for (k = 2; k < c.length; k += 2) { + ctx.lineTo(c[k] + dx, c[k + 1] + dy); + } + if (this.fill) { + ctx.strokeStyle = this.fill; + } + + ctx.stroke(); + break; + case 'polygon': + ctx.beginPath(); + ctx.moveTo(c[0] + dx, c[1] + dy); + for (k = 2; k < c.length; k += 2) { + ctx.lineTo(c[k] + dx, c[k + 1] + dy); + } + ctx.closePath(); + if (item.fill !== "") { + ctx.fillStyle = item.fill; + ctx.strokeStyle = item.fill; + ctx.fill(); + } + ctx.stroke(); + break; + case 'image': + break; + } + } +} + +Turtle.prototype.get_width = function () { + return this.canvas[0].width; +} + +Turtle.prototype.get_height = function () { + return this.canvas[0].height; +} + +Turtle.prototype.delete = function (item) { + if (item == 'all') { + this.items = []; + } else { + delete this.items[item]; + } +} + +Turtle.prototype.create_image = function (image) { + this.items.push({type:'image',image:image}); + return this.items.length - 1; +} + +Turtle.prototype.create_line = function () { + this.items.push({type:'line', + fill: '', + coords:[0,0,0,0], + width:2, + capstyle:'round'}); + return this.items.length - 1; +} + +Turtle.prototype.create_polygon = function () { + this.items.push({type:'polygon', + // fill: "" XXX + // outline: "" XXX + coords:[0,0,0,0,0,0] + }); + return this.items.length - 1; +} + +// XXX might make this varargs as in Tkinter +Turtle.prototype.coords = function (item, coords) { + if (coords === undefined) { + return this.items[item].coords; + } + this.items[item].coords = coords; +} + +Turtle.prototype.itemconfigure = function (item, key, value) { + this.items[item][key] = value; +} + +// value might be undefined +Turtle.prototype.css = function (key, value) { + if (value === undefined) { + return this.canvas.css(key); + } else { + // jQuery return value is confusing when the css is set + this.canvas.css(key, value); + } +} + +function run(launchmsg) { + var i, turtlescreen, msg, result, cmd; + $('#assess').empty(); + + turtlescreen = new Turtle(); + + output = $('#output'); + output.empty(); + if (typeof pipeurl === 'undefined') { + if (wp_port == '443') { + pipeurl = 'wss://'+wp_hostname+'/pipe'; + } else { + pipeurl = 'ws://'+wp_hostname+':'+wp_port+'/pipe'; + } + } + saveFile(); + output.pipe = new WebSocket(pipeurl); + output.pipe.onopen = function () { + output.pipe.send(JSON.stringify(launchmsg)); + }; + output.pipe.onmessage = function (response) { + msg = JSON.parse(response.data); + if (msg.cmd == 'input') { + output.inputelem = $('',{'size':40}); + submit = $('',{'type':'submit'}); + submit.click(function (){ + text = output.inputelem.val(); + output.input.replaceWith($('', {text:text+'\n'})); + output.pipe.send(JSON.stringify({'cmd':'inputresult', + 'data':text})); + }); + output.inputelem.keydown(function(event){ + if(event.keyCode == 13){ + submit.click(); + } + }); + output.append($('', {text:msg.data})); + output.input = $('').append(output.inputelem).append(submit); + output.append(output.input); + output.inputelem.focus(); + } else if (msg.cmd == 'stop') { + if (launchmsg.cmd == 'runscript') { + if (msg.timedout) { + output.append('
Dein Programm hat zu lange gerechnet und wurde beendet.'); + } else { + output.append('
Dein Progamm wurde beendet'); + } + } + output.pipe.close(); + } else if (msg.cmd == 'passed') { + $('#assess').html("Herzlich Glückwunsch! Dein Programm funktioniert korrekt."); + } else if (msg.cmd == 'failed') { + $('#assess').html(msg.data); + } else if (msg.cmd == 'turtle') { + if (msg.action in turtlescreen) { + result = turtlescreen[msg.action].apply(turtlescreen, msg.args); + output.pipe.send(JSON.stringify({cmd:'result', 'result':result})); + } else { + output.pipe.send(JSON.stringify({cmd:'exception', exception:'AttributeError', + message:msg.action})); + } + } else if (msg.cmd == 'turtlebatch') { + for (i=0; i < msg.batch.length; i += 1) { + cmd = msg.batch[i]; + turtlescreen[cmd[0]].apply(turtlescreen, cmd[1]); + } + } else { + if(msg.stream == 'internal') { + output.append('
Interner Fehler (bitte melden):\n'); + } + else if (msg.stream == 'stderr') { + showConsole(); + $('#consoleradio').prop('checked', 'checked'); + } + output.append($('',{text:msg.data, 'class':msg.stream})); + } + }; +} diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index b77e4936..81a25b0a 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -4,6 +4,7 @@ class SubmissionsController < ApplicationController include Lti include SubmissionParameters include SubmissionScoring + include Tubesock::Hijack before_action :set_submission, only: [:download_file, :render_file, :run, :score, :show, :statistics, :stop, :test] before_action :set_docker_client, only: [:run, :test] @@ -70,20 +71,58 @@ class SubmissionsController < ApplicationController end def run - with_server_sent_events do |server_sent_event| - output = @docker_client.execute_run_command(@submission, params[:filename]) - - server_sent_event.write({stdout: output[:stdout]}, event: 'output') if output[:stdout] - server_sent_event.write({stderr: output[:stderr]}, event: 'output') if output[:stderr] - - server_sent_event.write({status: output[:status]}, event: 'status') - - unless output[:stderr].nil? - if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(output[:stderr]) - server_sent_event.write(hint, event: 'hint') - else - store_error(output[:stderr]) + # with_server_sent_events do |server_sent_event| + # output = @docker_client.execute_run_command(@submission, params[:filename]) + + # server_sent_event.write({stdout: output[:stdout]}, event: 'output') if output[:stdout] + # server_sent_event.write({stderr: output[:stderr]}, event: 'output') if output[:stderr] + + # server_sent_event.write({status: output[:status]}, event: 'status') + + # unless output[:stderr].nil? + # if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(output[:stderr]) + # server_sent_event.write(hint, event: 'hint') + # else + # store_error(output[:stderr]) + # end + # end + # end + + hijack do |tubesock| + Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? + + result = @docker_client.execute_run_command(@submission, params[:filename]) + socket = result[:socket] + + socket.on :message do |event| + puts "Docker sending: " + event.data + parse_message(event.data, 'stdout', tubesock) + end + + tubesock.onmessage do |data| + puts "Client sending: " + data + res = socket.send data + if res == false + puts "Something is wrong." + end + end + end + end + + def parse_message(message, output_stream, socket, recursive = true) + begin + parsed = JSON.parse(message) + socket.send_data message + rescue JSON::ParserError => e + print "1\n" + if ((recursive == true) && (message.include? "\n")) + print "3\n" + for part in message.split("\n") + self.parse_message(part,output_stream,socket,false) end + else + parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>message} + socket.send_data JSON.dump(parsed) end end end diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 33a1d6c3..b0a3f0e0 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -45,12 +45,24 @@ .panel.panel-warning .panel-heading = t('.hint') .panel-body - #output - pre = t('.no_output_yet') - - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] - #flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' - .panel-heading = 'Gain more insights here' - .panel-body + .row + #output-col1 + // todo set to full width if turtle isnt used + #prompt.input-group.hidden + span.input-group-addon = 'Your input' + input#prompt-input.form-control type='text' + span.input-group-btn + button#prompt-submit.btn.btn-primary type="button" = 'Send' + #output + pre = t('.no_output_yet') + - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] + #flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' + .panel-heading = 'Gain more insights here' + .panel-body + #output-col2.col-lg-5.col-md-5 + #turtlediv + // todo what should the canvas default size be? + canvas#turtlecanvas.hidden style='border-style:solid;border-width:thin' #progress.tab-pane #results h2 = t('.results') @@ -79,4 +91,4 @@ - if qa_url #questions-column #questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}" - = qa_js_tag \ No newline at end of file + = qa_js_tag diff --git a/lib/docker_client.rb b/lib/docker_client.rb index 58984ddc..3b39c71b 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -11,6 +11,7 @@ class DockerClient RETRY_COUNT = 2 attr_reader :container + attr_reader :socket def self.check_availability! Timeout.timeout(config[:connection_timeout]) { Docker.version } @@ -41,7 +42,12 @@ class DockerClient 'Memory' => execution_environment.memory_limit.megabytes, 'NetworkDisabled' => !execution_environment.network_enabled?, 'OpenStdin' => true, - 'StdinOnce' => true + 'StdinOnce' => true, + # required to expose standard streams over websocket + 'AttachStdout' => true, + 'AttachStdin' => true, + 'AttachStderr' => true, + 'Tty' => true } end @@ -52,6 +58,29 @@ class DockerClient } end + def create_socket(container, stderr=false) + # todo factor out query params + # todo separate stderr + query_params = 'logs=1&stream=1&' + (stderr ? 'stderr=1' : 'stdout=1&stdin=1') + + # Headers are required by Docker + headers = {'Origin' => 'http://localhost'} + + socket = Faye::WebSocket::Client.new(DockerClient.config['ws_host'] + '/containers/' + @container.id + '/attach/ws?' + query_params, [], :headers => headers) + + socket.on :error do |event| + Rails.logger.info "Websocket error: " + event.message + end + socket.on :close do |event| + Rails.logger.info "Websocket closed." + end + socket.on :open do |event| + Rails.logger.info "Websocket created." + kill_after_timeout(container) + end + socket + end + def copy_file_to_workspace(options = {}) FileUtils.cp(options[:file].native_file.path, local_file_path(options)) end @@ -118,14 +147,66 @@ class DockerClient #(tries += 1) <= RETRY_COUNT ? retry : raise(error) end - [:run, :test].each do |cause| - define_method("execute_#{cause}_command") do |submission, filename, &block| - command = submission.execution_environment.send(:"#{cause}_command") % command_substitutions(filename) - create_workspace_files = proc { create_workspace_files(container, submission) } - execute_command(command, create_workspace_files, block) + def execute_websocket_command(command, before_execution_block, output_consuming_block) + @container = DockerContainerPool.get_container(@execution_environment) + if @container + before_execution_block.try(:call) + # todo catch exception if socket could not be created + @socket ||= create_socket(@container) + # Newline required to flush + @socket.send command + "\n" + {status: :container_running, socket: @socket} + else + {status: :container_depleted} end end + def kill_after_timeout(container) + """ + We need to start a second thread to kill the websocket connection, + as it is impossible to determine when no more input is requested. + """ + Thread.new do + timeout = @execution_environment.permitted_execution_time.to_i # seconds + sleep(timeout) + Rails.logger.info("Killing container after timeout of " + timeout.to_s + " seconds.") + # if we use pooling and recylce the containers, put it back. otherwise, destroy it. + # (DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container) + + # todo won't this always create a new container? + # remove container from pool, then destroy it + (DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) : + + # destroy container + self.class.destroy_container(container) + + # if we recylce containers, we start a fresh one + if(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) + # create new container and add it to @all_containers and @containers. + container = self.class.create_container(@execution_environment) + DockerContainerPool.add_to_all_containers(container, @execution_environment) + end + end + end + + def execute_run_command(submission, filename, &block) + """ + Run commands by attaching a websocket to Docker. + """ + command = submission.execution_environment.send(:"run_command") % command_substitutions(filename) + create_workspace_files = proc { create_workspace_files(container, submission) } + execute_websocket_command(command, create_workspace_files, block) + end + + def execute_test_command(subbmission, filename, &block) + """ + Stick to existing Docker API with exec command. + """ + command = submission.execution_environment.send(:"test_command") % command_substitutions(filename) + create_workspace_files = proc { create_workspace_files(container, submission) } + execute_command(command, create_workspace_files, block) + end + def self.find_image_by_tag(tag) Docker::Image.all.detect { |image| image.info['RepoTags'].flatten.include?(tag) } end From e6eeebfd4b4df3e6f865dfebd10d2644ffa62efd Mon Sep 17 00:00:00 2001 From: Janusch Jacoby Date: Tue, 15 Sep 2015 19:34:22 +0200 Subject: [PATCH 02/21] Filter and colour output, handle exit properly --- app/assets/javascripts/editor.js | 115 +++++++++++++--------- app/controllers/submissions_controller.rb | 36 +++++-- 2 files changed, 95 insertions(+), 56 deletions(-) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 5186e3d0..838b17a2 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -25,9 +25,11 @@ $(function() { numMessages = 0, turtlecanvas = $('#turtlecanvas'), prompt = $('#prompt'), - commands = ['input', 'write', 'turtle', 'turtlebatch'], + commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit'], streams = ['stdin', 'stdout', 'stderr']; + var ENTER_KEY_CODE = 13; + var flowrResultHtml = '
' var ajax = function(options) { @@ -190,6 +192,7 @@ $(function() { var evaluateCodeWithStreamedResponse = function(url, callback) { initWebsocketConnection(url); + console.log(callback); // TODO only init turtle when required initTurtle(); @@ -645,11 +648,11 @@ $(function() { }; var initializeWorkspaceButtons = function() { - $('#assess').on('click', scoreCode); + $('#assess').on('click', scoreCode); // todo $('#dropdown-render, #render').on('click', renderCode); $('#dropdown-run, #run').on('click', runCode); - $('#dropdown-stop, #stop').on('click', stopCode); - $('#dropdown-test, #test').on('click', testCode); + $('#dropdown-stop, #stop').on('click', stopCode); // todo + $('#dropdown-test, #test').on('click', testCode); // todo $('#save').on('click', saveCode); $('#start-over').on('click', confirmReset); }; @@ -690,6 +693,7 @@ $(function() { }; var isBrowserSupported = function() { + // todo event streams are no longer required with websockets return window.EventSource !== undefined; }; @@ -717,12 +721,16 @@ $(function() { chunkBuffer.push(output); } } else { + resetOutputTab(); + } + }; + + var resetOutputTab = function() { clearOutput(); $('#hint').fadeOut(); $('#flowrHint').fadeOut(); showTab(2); - } - }; + } var printOutput = function(output, colorize, index) { var element = findOrCreateOutputElement(index); @@ -1010,21 +1018,43 @@ $(function() { var stopCode = function(event) { event.preventDefault(); if ($('#stop').is(':visible')) { - var jqxhr = ajax({ - data: { - container_id: $('#stop').data('container').id - }, - url: $('#stop').data('url') - }); - jqxhr.always(function() { - hideSpinner(); - running = false; - toggleButtonStates(); - }); - jqxhr.fail(ajaxError); + killWebsocket(); + stopContainer(); } }; + // todo we are missing the url here + // we could also hide the container completely by killing it on the server and only exposing the websocket + var stopContainer = function() { + var jqxhr = ajax({ + data: { + container_id: $('#stop').data('container').id + }, + url: $('#stop').data('url') + }); + jqxhr.always(function() { + hideSpinner(); + running = false; + toggleButtonStates(); + }); + jqxhr.fail(ajaxError); + } + + var killWebsocket = function() { + if (websocket.readyState != WebSocket.OPEN) { + return; + } + // todo flash notification + websocket.send(JSON.stringify({cmd: 'exit'})); + websocket.flush(); + websocket.close(); + // todo remove this once xhr works or container is killed on the server + hideSpinner(); + running = false; + toggleButtonStates(); + } + + // todo set this from websocket command, required to e.g. stop container var storeContainerInformation = function(event) { var container_information = JSON.parse(event.data); $('#stop').data('container', container_information); @@ -1089,15 +1119,16 @@ $(function() { }; var initTurtle = function() { - turtlescreen = new Turtle(websocket, $('#turtlecanvas')); + // todo guard clause if turtle is not required for the current exercise + turtlescreen = new Turtle(websocket, turtlecanvas); + if ($('#run').isPresent()) { + $('#run').bind('click', hideCanvas); + } }; - var initCallbacks = function() { + var initPrompt = function() { if ($('#run').isPresent()) { - $('#run').bind('click', function(event) { - hideCanvas(); - hidePrompt(); - }); + $('#run').bind('click', hidePrompt); } if ($('#prompt').isPresent()) { $('#prompt').on('keypress', handlePromptKeyPress); @@ -1106,23 +1137,22 @@ $(function() { } var onWebSocketOpen = function(evt) { - //alert("Session started"); + resetOutputTab(); }; var onWebSocketClose = function(evt) { - //alert("Session terminated"); + // no reason to alert since this will happen either way }; var onWebSocketMessage = function(evt) { - numMessages++; parseCanvasMessage(evt.data, true); }; var onWebSocketError = function(evt) { - //alert("Something went wrong.") + // todo flash error message }; - var executeCommand = function(msg) { + var executeWebsocketCommand = function(msg) { if ($.inArray(msg.cmd, commands) == -1) { console.log("Unknown command: " + msg.cmd); // skipping unregistered commands is required @@ -1144,26 +1174,16 @@ $(function() { showCanvas(); handleTurtlebatchCommand(msg); break; + case 'exit': + killWebsocket(); + break; } }; - // todo reuse method from editor.js var printWebsocketOutput = function(msg) { - var element = findOrCreateOutputElement(0); - console.log(element); - switch (msg.stream) { - case 'internal': - element.addClass('text-danger'); - break; - case 'stderr': - element.addClass('text-warning'); - break; - case 'stdout': - case 'stdin': // for eventual prompts - default: - element.addClass('text-muted'); - } - element.append(msg.data) + var stream = {}; + stream[msg.stream] = msg.data; + printOutput(stream, true, 0); }; var handleTurtleCommand = function(msg) { @@ -1190,6 +1210,7 @@ $(function() { } var submitPromptInput = function() { + // todo make sure websocket is actually open var input = $('#prompt-input'); var message = input.val(); websocket.send(JSON.stringify({cmd: 'result', 'data': message})); @@ -1219,7 +1240,7 @@ $(function() { } return; } - executeCommand(msg); + executeWebsocketCommand(msg); }; var showPrompt = function() { @@ -1231,7 +1252,6 @@ $(function() { var hidePrompt = function() { if (prompt.isPresent() && !prompt.hasClass('hidden')) { - console.log("hiding prompt2"); prompt.addClass('hidden'); } } @@ -1314,6 +1334,7 @@ $(function() { initializeEventHandlers(); initializeFileTree(); initializeTooltips(); + initPrompt(); renderScore(); showFirstFile(); showRequestedTab(); diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 81a25b0a..a84f069e 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -71,6 +71,7 @@ class SubmissionsController < ApplicationController end def run + # TODO reimplement SSEs with websocket commands # with_server_sent_events do |server_sent_event| # output = @docker_client.execute_run_command(@submission, params[:filename]) @@ -95,16 +96,35 @@ class SubmissionsController < ApplicationController socket = result[:socket] socket.on :message do |event| - puts "Docker sending: " + event.data - parse_message(event.data, 'stdout', tubesock) + Rails.logger.info("Docker sending: " + event.data) + handle_message(event.data, tubesock) + end + + socket.on :close do |event| + kill_socket(tubesock) end tubesock.onmessage do |data| - puts "Client sending: " + data - res = socket.send data - if res == false - puts "Something is wrong." - end + Rails.logger.info("Client sending: " + data) + socket.send data + end + end + end + + def kill_socket(tubesock) + # Hijacked connection needs to be notified correctly + tubesock.send_data JSON.dump({'cmd' => 'exit'}) + tubesock.close + end + + def handle_message(message, tubesock) + # Handle special commands first + if (/^exit/.match(message)) + kill_socket(tubesock) + else + # Filter out information about user and working directory + if !(/root|workspace/.match(message)) + parse_message(message, 'stdout', tubesock) end end end @@ -114,9 +134,7 @@ class SubmissionsController < ApplicationController parsed = JSON.parse(message) socket.send_data message rescue JSON::ParserError => e - print "1\n" if ((recursive == true) && (message.include? "\n")) - print "3\n" for part in message.split("\n") self.parse_message(part,output_stream,socket,false) end From f21310e5fe7e48ff0a695dc461daadea905a18e9 Mon Sep 17 00:00:00 2001 From: Janusch Jacoby Date: Tue, 15 Sep 2015 19:56:10 +0200 Subject: [PATCH 03/21] Add webpython + instructions --- webpython/Dockerfile | 11 + webpython/Makefile | 58 + webpython/README.md | 47 + webpython/turtle.py | 4078 ++++++++++++++++++++++++++++++++++++++++ webpython/webpython.py | 174 ++ 5 files changed, 4368 insertions(+) create mode 100644 webpython/Dockerfile create mode 100644 webpython/Makefile create mode 100644 webpython/README.md create mode 100644 webpython/turtle.py create mode 100644 webpython/webpython.py diff --git a/webpython/Dockerfile b/webpython/Dockerfile new file mode 100644 index 00000000..978d68b0 --- /dev/null +++ b/webpython/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu:14.04 +MAINTAINER "Martin v. Löwis" +RUN locale-gen en_US.UTF-8 +ENV LANG en_US.UTF-8 +ADD webpython.py /usr/lib/python3.4/webpython.py +RUN rm /usr/lib/python3.4/turtle.py +ADD turtle.py /usr/lib/python3.4/turtle.py +ADD assess /usr/lib/python3.4/assess +RUN adduser --disabled-password --gecos Python python +USER python +WORKDIR /home/python diff --git a/webpython/Makefile b/webpython/Makefile new file mode 100644 index 00000000..76f102b7 --- /dev/null +++ b/webpython/Makefile @@ -0,0 +1,58 @@ +CODEMIRROR=html/js/codemirror.js + +all: logs + +logs: + mkdir logs + +# These are put into source control +generated: $(CODEMIRROR) + + +$(CODEMIRROR): CodeMirror/lib/codemirror.js CodeMirror/mode/python/python.js + (cd CodeMirror; bin/compress codemirror python) > $@ + +run: all kill + twistd -l logs/webpython.log -y webpython.tac + +runpy: killpy + twistd --pidfile pylaunch.pid -l logs/pylaunch.log -y pylaunch.tac + +manager: all killmanager + twistd --pidfile manager.pid -l logs/manager.log -y manager.tac + +kill: + if [ -f twistd.pid ];\ + then\ + kill `cat twistd.pid`;\ + fi + +killpy: + if [ -f pylaunch.pid ];\ + then\ + kill `cat pylaunch.pid`;\ + fi + +killmanager: + if [ -f manager.pid ];\ + then\ + kill `cat manager.pid`;\ + fi + +docker: + docker.io build --rm -t webpython . + +update: + git pull + make docker + service webpython-worker restart + +rmexited: + docker.io ps -a|grep 'Exit '|awk '{print $$1;}'|xargs docker.io rm + +rmi: + docker.io rmi $$(docker.io images | grep "^" | awk '{print $$3;}') + +killold: + -docker.io ps | egrep 'hours? ago' |awk '{print $$1}'|xargs docker.io kill + -killall -o 30m -9 python3 diff --git a/webpython/README.md b/webpython/README.md new file mode 100644 index 00000000..e95eb83d --- /dev/null +++ b/webpython/README.md @@ -0,0 +1,47 @@ +Local setup +=========== + +1. `git checkout webpython-hybrid` +2. Make sure to install all dependencies and migrations: + + rake db:migrate + bundle install + +3. Create a new docker image containing the Turtle library and the i/o wrapper: + + cd webpython + docker build -t IMAGE_NAME . + +4. Configure your Docker host at `config/docker.yml.erb`. Make sure to add a websocket host, for example like this (this is probably different for you): + + host: tcp://localhost:2375 + ws_host: ws://localhost:2375 + +5. Run the CodeOcean server with `rails s -p 3333` + +6. Login with admin@example.org (pw: admin) and create a new execution environment picking the newly created Docker image from the dropdown. Set the initial command to: + + cd /usr/lib/python3.4 && python3 webpython.py + +7. Create a new exercise for the newly created execution environment with an arbritrary main file. +8. Implement the exercise. The code below can be used as an example to see the canvas and I/O in action: + + import turtle + wn = turtle.Screen() + alex = turtle.Turtle() + + # i/o test + print("hello!") + print("please enter your name") + name = input() + print("your name is", name) + + # canvas test + alex.forward(50) + alex.right(90) + alex.forward(30) + alex.right(90) + alex.forward(30) + + wn.mainloop() + diff --git a/webpython/turtle.py b/webpython/turtle.py new file mode 100644 index 00000000..87e91955 --- /dev/null +++ b/webpython/turtle.py @@ -0,0 +1,4078 @@ +# +# turtle.py: a Tkinter based turtle graphics module for Python +# Version 1.1b - 4. 5. 2009 +# +# Copyright (C) 2006 - 2010 Gregor Lingl +# email: glingl@aon.at +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + + +""" +Turtle graphics is a popular way for introducing programming to +kids. It was part of the original Logo programming language developed +by Wally Feurzig and Seymour Papert in 1966. + +Imagine a robotic turtle starting at (0, 0) in the x-y plane. After an ``import turtle``, give it +the command turtle.forward(15), and it moves (on-screen!) 15 pixels in +the direction it is facing, drawing a line as it moves. Give it the +command turtle.right(25), and it rotates in-place 25 degrees clockwise. + +By combining together these and similar commands, intricate shapes and +pictures can easily be drawn. + +----- turtle.py + +This module is an extended reimplementation of turtle.py from the +Python standard distribution up to Python 2.5. (See: http://www.python.org) + +It tries to keep the merits of turtle.py and to be (nearly) 100% +compatible with it. This means in the first place to enable the +learning programmer to use all the commands, classes and methods +interactively when using the module from within IDLE run with +the -n switch. + +Roughly it has the following features added: + +- Better animation of the turtle movements, especially of turning the + turtle. So the turtles can more easily be used as a visual feedback + instrument by the (beginning) programmer. + +- Different turtle shapes, gif-images as turtle shapes, user defined + and user controllable turtle shapes, among them compound + (multicolored) shapes. Turtle shapes can be stretched and tilted, which + makes turtles very versatile geometrical objects. + +- Fine control over turtle movement and screen updates via delay(), + and enhanced tracer() and speed() methods. + +- Aliases for the most commonly used commands, like fd for forward etc., + following the early Logo traditions. This reduces the boring work of + typing long sequences of commands, which often occur in a natural way + when kids try to program fancy pictures on their first encounter with + turtle graphics. + +- Turtles now have an undo()-method with configurable undo-buffer. + +- Some simple commands/methods for creating event driven programs + (mouse-, key-, timer-events). Especially useful for programming games. + +- A scrollable Canvas class. The default scrollable Canvas can be + extended interactively as needed while playing around with the turtle(s). + +- A TurtleScreen class with methods controlling background color or + background image, window and canvas size and other properties of the + TurtleScreen. + +- There is a method, setworldcoordinates(), to install a user defined + coordinate-system for the TurtleScreen. + +- The implementation uses a 2-vector class named Vec2D, derived from tuple. + This class is public, so it can be imported by the application programmer, + which makes certain types of computations very natural and compact. + +- Appearance of the TurtleScreen and the Turtles at startup/import can be + configured by means of a turtle.cfg configuration file. + The default configuration mimics the appearance of the old turtle module. + +- If configured appropriately the module reads in docstrings from a docstring + dictionary in some different language, supplied separately and replaces + the English ones by those read in. There is a utility function + write_docstringdict() to write a dictionary with the original (English) + docstrings to disc, so it can serve as a template for translations. + +Behind the scenes there are some features included with possible +extensions in mind. These will be commented and documented elsewhere. + +""" + +_ver = "turtle 1.1b- - for Python 3.1 - 4. 5. 2009" + +# print(_ver) + +import types +import math +import time +import inspect +import sys +import builtins + +from os.path import isfile, split, join +from copy import deepcopy + +_tg_classes = ['TurtleScreen', 'Screen', + 'RawTurtle', 'Turtle', 'RawPen', 'Pen', 'Shape', 'Vec2D'] +_tg_screen_functions = ['addshape', 'bgcolor', 'bgpic', 'bye', + 'clearscreen', 'colormode', 'delay', 'exitonclick', 'getcanvas', + 'getshapes', 'listen', 'mainloop', 'mode', 'numinput', + 'onkey', 'onkeypress', 'onkeyrelease', 'onscreenclick', 'ontimer', + 'register_shape', 'resetscreen', 'screensize', 'setup', + 'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles', 'update', + 'window_height', 'window_width'] +_tg_turtle_functions = ['back', 'backward', 'begin_fill', 'begin_poly', 'bk', + 'circle', 'clear', 'clearstamp', 'clearstamps', 'clone', 'color', + 'degrees', 'distance', 'dot', 'down', 'end_fill', 'end_poly', 'fd', + 'fillcolor', 'filling', 'forward', 'get_poly', 'getpen', 'getscreen', 'get_shapepoly', + 'getturtle', 'goto', 'heading', 'hideturtle', 'home', 'ht', 'isdown', + 'isvisible', 'left', 'lt', 'onclick', 'ondrag', 'onrelease', 'pd', + 'pen', 'pencolor', 'pendown', 'pensize', 'penup', 'pos', 'position', + 'pu', 'radians', 'right', 'reset', 'resizemode', 'rt', + 'seth', 'setheading', 'setpos', 'setposition', 'settiltangle', + 'setundobuffer', 'setx', 'sety', 'shape', 'shapesize', 'shapetransform', 'shearfactor', 'showturtle', + 'speed', 'st', 'stamp', 'tilt', 'tiltangle', 'towards', + 'turtlesize', 'undo', 'undobufferentries', 'up', 'width', + 'write', 'xcor', 'ycor'] +_tg_utilities = ['write_docstringdict', 'done'] + +__all__ = (_tg_classes + _tg_screen_functions + _tg_turtle_functions + + _tg_utilities + ['Terminator']) # + _math_functions) + +_alias_list = ['addshape', 'backward', 'bk', 'fd', 'ht', 'lt', 'pd', 'pos', + 'pu', 'rt', 'seth', 'setpos', 'setposition', 'st', + 'turtlesize', 'up', 'width'] + +_CFG = {"width" : 0.5, # Screen + "height" : 0.75, + "canvwidth" : 400, + "canvheight": 300, + "leftright": None, + "topbottom": None, + "mode": "standard", # TurtleScreen + "colormode": 1.0, + "delay": 10, + "undobuffersize": 1000, # RawTurtle + "shape": "classic", + "pencolor" : "black", + "fillcolor" : "black", + "resizemode" : "noresize", + "visible" : True, + "language": "english", # docstrings + "exampleturtle": "turtle", + "examplescreen": "screen", + "title": "Python Turtle Graphics", + "using_IDLE": False + } + +def config_dict(filename): + """Convert content of config-file into dictionary.""" + with open(filename, "r") as f: + cfglines = f.readlines() + cfgdict = {} + for line in cfglines: + line = line.strip() + if not line or line.startswith("#"): + continue + try: + key, value = line.split("=") + except: + print("Bad line in config-file %s:\n%s" % (filename,line)) + continue + key = key.strip() + value = value.strip() + if value in ["True", "False", "None", "''", '""']: + value = eval(value) + else: + try: + if "." in value: + value = float(value) + else: + value = int(value) + except: + pass # value need not be converted + cfgdict[key] = value + return cfgdict + +def readconfig(cfgdict): + """Read config-files, change configuration-dict accordingly. + + If there is a turtle.cfg file in the current working directory, + read it from there. If this contains an importconfig-value, + say 'myway', construct filename turtle_mayway.cfg else use + turtle.cfg and read it from the import-directory, where + turtle.py is located. + Update configuration dictionary first according to config-file, + in the import directory, then according to config-file in the + current working directory. + If no config-file is found, the default configuration is used. + """ + default_cfg = "turtle.cfg" + cfgdict1 = {} + cfgdict2 = {} + if isfile(default_cfg): + cfgdict1 = config_dict(default_cfg) + if "importconfig" in cfgdict1: + default_cfg = "turtle_%s.cfg" % cfgdict1["importconfig"] + try: + head, tail = split(__file__) + cfg_file2 = join(head, default_cfg) + except: + cfg_file2 = "" + if isfile(cfg_file2): + cfgdict2 = config_dict(cfg_file2) + _CFG.update(cfgdict2) + _CFG.update(cfgdict1) + +try: + readconfig(_CFG) +except: + print ("No configfile read, reason unknown") + + +class Vec2D(tuple): + """A 2 dimensional vector class, used as a helper class + for implementing turtle graphics. + May be useful for turtle graphics programs also. + Derived from tuple, so a vector is a tuple! + + Provides (for a, b vectors, k number): + a+b vector addition + a-b vector subtraction + a*b inner product + k*a and a*k multiplication with scalar + |a| absolute value of a + a.rotate(angle) rotation + """ + def __new__(cls, x, y): + return tuple.__new__(cls, (x, y)) + def __add__(self, other): + return Vec2D(self[0]+other[0], self[1]+other[1]) + def __mul__(self, other): + if isinstance(other, Vec2D): + return self[0]*other[0]+self[1]*other[1] + return Vec2D(self[0]*other, self[1]*other) + def __rmul__(self, other): + if isinstance(other, int) or isinstance(other, float): + return Vec2D(self[0]*other, self[1]*other) + def __sub__(self, other): + return Vec2D(self[0]-other[0], self[1]-other[1]) + def __neg__(self): + return Vec2D(-self[0], -self[1]) + def __abs__(self): + return (self[0]**2 + self[1]**2)**0.5 + def rotate(self, angle): + """rotate self counterclockwise by angle + """ + perp = Vec2D(-self[1], self[0]) + angle = angle * math.pi / 180.0 + c, s = math.cos(angle), math.sin(angle) + return Vec2D(self[0]*c+perp[0]*s, self[1]*c+perp[1]*s) + def __getnewargs__(self): + return (self[0], self[1]) + def __repr__(self): + return "(%.2f,%.2f)" % self + + +############################################################################## +### From here up to line : Tkinter - Interface for turtle.py ### +### May be replaced by an interface to some different graphics toolkit ### +############################################################################## +import time + +# guaranteed to exist in Tk +namedcolors = ["white", "black", "red", "green", "blue", "cyan", "yellow", "magenta"] + +class WebImage: + pass + +class BlankImage(WebImage): + pass + +class Event: + def __init__(self, d): + for k,v in d.items(): + setattr(self, k, v) + +class WebCanvas: + def __init__(self, shell): + self.shell = shell + self.bindings = {} + self.batch = [] + self.items = [] + + def addbatch(self, msg, *params): + self.batch.append([msg,params]) + + def flushbatch(self): + if not self.batch: + return + self.shell.sendpickle({'cmd':'turtlebatch', + 'batch':self.batch}) + self.batch = [] + + # Locally implemented + def after(self, delay): + time.sleep(delay/1000) + + def tag_bind(self, item, event, function, add): + self.bindings[event] = function + + def tag_unbind(self, event): + del self.bindings[event] + + def canvasx(self, x): + return x + + def canvasy(self, y): + return y + + # batched + def create_image(self, w, h, image): + self.items.append({'type':'image','image':image}) + self.addbatch('create_image', image) + return len(self.items)-1 + + def create_line(self): + self.items.append({'type':'line', + 'fill': '', + 'coords':[0,0,0,0], + 'width':2, + 'capstyle':'round'}) + self.addbatch('create_line') + return len(self.items)-1 + + def create_polygon(self): + self.items.append({'type':'polygon', + 'coords':[0,0,0,0,0,0] + }) + self.addbatch('create_polygon') + return len(self.items)-1 + + def coords(self, item, coords=None): + if coords is None: + return self.items[item]['coords'] + self.items[item]['coords'] = coords + self.addbatch('coords', item, coords) + + def itemconfigure(self, item, **args): + assert len(args) == 1 + key, value = list(args.items())[0] + self.items[item][key] = value + self.addbatch('itemconfigure', item, key, value) + + def update(self): + self.addbatch('update') + self.flushbatch() + + # XXX TODO + def tag_raise(self, item): + pass + + def __getattr__(self, attr): + def call(*args): + self.flushbatch() + self.shell.sendpickle({'cmd':'turtle', + 'action':attr, + 'args':args}) + result = self.shell.receivemsg() + if result['cmd'] == 'result': + return result.get('result') # might be None as JSON leaves out undefined values + else: + raise getattr(builtins, result['exception'])(result['message']) + return call + + def getevent(self): + msg = self.shell.receivecanvas() + event = Event(msg) + if event.type in self.bindings: + self.bindings[event.type](event) + + +class TurtleScreenBase(object): + """Provide the basic graphics functionality. + Interface between Tkinter and turtle.py. + + To port turtle.py to some different graphics toolkit + a corresponding TurtleScreenBase class has to be implemented. + """ + + @staticmethod + def _blankimage(): + """return a blank image object + """ + return BlankImage() + + @staticmethod + def _image(filename): + """return an image object containing the + imagedata from a gif-file named filename. + """ + raise IOError("opening files is not permitted in this implementation") + + def __init__(self, cv): + self.cv = cv + self.canvwidth = cv.get_width() + self.canvheight = cv.get_height() + self.xscale = self.yscale = 1.0 + + def _createpoly(self): + """Create an invisible polygon item on canvas self.cv) + """ + return self.cv.create_polygon() + + def _drawpoly(self, polyitem, coordlist, fill=None, + outline=None, width=None, top=False): + """Configure polygonitem polyitem according to provided + arguments: + coordlist is sequence of coordinates + fill is filling color + outline is outline color + top is a boolean value, which specifies if polyitem + will be put on top of the canvas' displaylist so it + will not be covered by other items. + """ + cl = [] + for x, y in coordlist: + cl.append(x * self.xscale) + cl.append(-y * self.yscale) + self.cv.coords(polyitem, cl) + if fill is not None: + self.cv.itemconfigure(polyitem, fill=fill) + if outline is not None: + self.cv.itemconfigure(polyitem, outline=outline) + if width is not None: + self.cv.itemconfigure(polyitem, width=width) + if top: + self.cv.tag_raise(polyitem) + + def _createline(self): + """Create an invisible line item on canvas self.cv) + """ + return self.cv.create_line() + + def _drawline(self, lineitem, coordlist=None, + fill=None, width=None, top=False): + """Configure lineitem according to provided arguments: + coordlist is sequence of coordinates + fill is drawing color + width is width of drawn line. + top is a boolean value, which specifies if polyitem + will be put on top of the canvas' displaylist so it + will not be covered by other items. + """ + if coordlist is not None: + cl = [] + for x, y in coordlist: + cl.append(x * self.xscale) + cl.append(-y * self.yscale) + self.cv.coords(lineitem, cl) + if fill is not None: + self.cv.itemconfigure(lineitem, fill=fill) + if width is not None: + self.cv.itemconfigure(lineitem, width=width) + if top: + self.cv.tag_raise(lineitem) + + def _delete(self, item): + """Delete graphics item from canvas. + If item is"all" delete all graphics items. + """ + self.cv.delete(item) + + def _update(self): + """Redraw graphics items on canvas + """ + self.cv.update() + + def _delay(self, delay): + """Delay subsequent canvas actions for delay ms.""" + self.cv.after(delay) + + def _iscolorstring(self, color): + """Check if the string color is a legal Tkinter color string. + """ + if color[0] == '#' and len(color) % 3 == 1: + try: + int(color[1:], 16) + except ValueError: + return False + return True + return color in namedcolors + + def _bgcolor(self, color=None): + """Set canvas' backgroundcolor if color is not None, + else return backgroundcolor.""" + if color is not None: + self.cv.css('background-color', color) + self._update() + else: + return self.cv.css('background-color') + + def _write(self, pos, txt, align, font, pencolor): + """Write txt at pos in canvas with specified font + and color. + Return text item and x-coord of right bottom corner + of text's bounding box.""" + x, y = pos + x = x * self.xscale + y = y * self.yscale + anchor = {"left":"sw", "center":"s", "right":"se" } + item = self.cv.create_text(x-1, -y, text = txt, anchor = anchor[align], + fill = pencolor, font = font) + x0, y0, x1, y1 = self.cv.bbox(item) + self.cv.update() + return item, x1-1 + +## def _dot(self, pos, size, color): +## """may be implemented for some other graphics toolkit""" + + def _onclick(self, item, fun, num=1, add=None): + """Bind fun to mouse-click event on turtle. + fun must be a function with two arguments, the coordinates + of the clicked point on the canvas. + num, the number of the mouse-button defaults to 1 + """ + if fun is None: + self.cv.tag_unbind(item, "" % num) + else: + def eventfun(event): + x, y = (self.cv.canvasx(event.x)/self.xscale, + -self.cv.canvasy(event.y)/self.yscale) + fun(x, y) + self.cv.tag_bind(item, "" % num, eventfun, add) + + def _onrelease(self, item, fun, num=1, add=None): + """Bind fun to mouse-button-release event on turtle. + fun must be a function with two arguments, the coordinates + of the point on the canvas where mouse button is released. + num, the number of the mouse-button defaults to 1 + + If a turtle is clicked, first _onclick-event will be performed, + then _onscreensclick-event. + """ + if fun is None: + self.cv.tag_unbind(item, "" % num) + else: + def eventfun(event): + x, y = (self.cv.canvasx(event.x)/self.xscale, + -self.cv.canvasy(event.y)/self.yscale) + fun(x, y) + self.cv.tag_bind(item, "" % num, + eventfun, add) + + def _ondrag(self, item, fun, num=1, add=None): + """Bind fun to mouse-move-event (with pressed mouse button) on turtle. + fun must be a function with two arguments, the coordinates of the + actual mouse position on the canvas. + num, the number of the mouse-button defaults to 1 + + Every sequence of mouse-move-events on a turtle is preceded by a + mouse-click event on that turtle. + """ + if fun is None: + self.cv.tag_unbind(item, "" % num) + else: + def eventfun(event): + try: + x, y = (self.cv.canvasx(event.x)/self.xscale, + -self.cv.canvasy(event.y)/self.yscale) + fun(x, y) + except: + pass + self.cv.tag_bind(item, "" % num, eventfun, add) + + def _onscreenclick(self, fun, num=1, add=None): + """Bind fun to mouse-click event on canvas. + fun must be a function with two arguments, the coordinates + of the clicked point on the canvas. + num, the number of the mouse-button defaults to 1 + + If a turtle is clicked, first _onclick-event will be performed, + then _onscreensclick-event. + """ + # XXX TODO + return + if fun is None: + self.cv.unbind("" % num) + else: + def eventfun(event): + x, y = (self.cv.canvasx(event.x)/self.xscale, + -self.cv.canvasy(event.y)/self.yscale) + fun(x, y) + self.cv.bind("" % num, eventfun, add) + + def _onkeyrelease(self, fun, key): + """Bind fun to key-release event of key. + Canvas must have focus. See method listen + """ + # XXX TODO + return + if fun is None: + self.cv.unbind("" % key, None) + else: + def eventfun(event): + fun() + self.cv.bind("" % key, eventfun) + + def _onkeypress(self, fun, key=None): + """If key is given, bind fun to key-press event of key. + Otherwise bind fun to any key-press. + Canvas must have focus. See method listen. + """ + # XXX TODO + return + if fun is None: + if key is None: + self.cv.unbind("", None) + else: + self.cv.unbind("" % key, None) + else: + def eventfun(event): + fun() + if key is None: + self.cv.bind("", eventfun) + else: + self.cv.bind("" % key, eventfun) + + def _listen(self): + """Set focus on canvas (in order to collect key-events) + """ + self.cv.focus_force() + + def _ontimer(self, fun, t): + """Install a timer, which calls fun after t milliseconds. + """ + if t == 0: + self.cv.after_idle(fun) + else: + self.cv.after(t, fun) + + def _createimage(self, image): + """Create and return image item on canvas. + """ + return self.cv.create_image(0, 0, image) + + def _drawimage(self, item, pos, image): + """Configure image item as to draw image object + at position (x,y) on canvas) + """ + x, y = pos + self.cv.coords(item, (x * self.xscale, -y * self.yscale)) + self.cv.itemconfig(item, image=image) + + def _setbgpic(self, item, image): + """Configure image item as to draw image object + at center of canvas. Set item to the first item + in the displaylist, so it will be drawn below + any other item .""" + self.cv.itemconfig(item, image=image) + self.cv.tag_lower(item) + + def _type(self, item): + """Return 'line' or 'polygon' or 'image' depending on + type of item. + """ + return self.cv.type(item) + + def _pointlist(self, item): + """returns list of coordinate-pairs of points of item + Example (for insiders): + >>> from turtle import * + >>> getscreen()._pointlist(getturtle().turtle._item) + [(0.0, 9.9999999999999982), (0.0, -9.9999999999999982), + (9.9999999999999982, 0.0)] + >>> """ + cl = self.cv.coords(item) + pl = [(cl[i], -cl[i+1]) for i in range(0, len(cl), 2)] + return pl + + def _setscrollregion(self, srx1, sry1, srx2, sry2): + self.cv.config(scrollregion=(srx1, sry1, srx2, sry2)) + + def _rescale(self, xscalefactor, yscalefactor): + items = self.cv.find_all() + for item in items: + coordinates = list(self.cv.coords(item)) + newcoordlist = [] + while coordinates: + x, y = coordinates[:2] + newcoordlist.append(x * xscalefactor) + newcoordlist.append(y * yscalefactor) + coordinates = coordinates[2:] + self.cv.coords(item, newcoordlist) + + def _resize(self, canvwidth=None, canvheight=None, bg=None): + """Resize the canvas the turtles are drawing on. Does + not alter the drawing window. + """ + # needs amendment + if not isinstance(self.cv, ScrolledCanvas): + return self.canvwidth, self.canvheight + if canvwidth is canvheight is bg is None: + return self.cv.canvwidth, self.cv.canvheight + if canvwidth is not None: + self.canvwidth = canvwidth + if canvheight is not None: + self.canvheight = canvheight + self.cv.reset(canvwidth, canvheight, bg) + + def _window_size(self): + """ Return the width and height of the turtle window. + """ + width = self.cv.winfo_width() + if width <= 1: # the window isn't managed by a geometry manager + width = self.cv['width'] + height = self.cv.winfo_height() + if height <= 1: # the window isn't managed by a geometry manager + height = self.cv['height'] + return width, height + + def mainloop(self): + """Starts event loop - calling Tkinter's mainloop function. + + No argument. + + Must be last statement in a turtle graphics program. + Must NOT be used if a script is run from within IDLE in -n mode + (No subprocess) - for interactive use of turtle graphics. + + Example (for a TurtleScreen instance named screen): + >>> screen.mainloop() + + """ + i = 1 + while True: + i = i+1 + self.cv.getevent() + + + def textinput(self, title, prompt): + """Pop up a dialog window for input of a string. + + Arguments: title is the title of the dialog window, + prompt is a text mostly describing what information to input. + + Return the string input + If the dialog is canceled, return None. + + Example (for a TurtleScreen instance named screen): + >>> screen.textinput("NIM", "Name of first player:") + + """ + # XXX TODO + return simpledialog.askstring(title, prompt) + + def numinput(self, title, prompt, default=None, minval=None, maxval=None): + """Pop up a dialog window for input of a number. + + Arguments: title is the title of the dialog window, + prompt is a text mostly describing what numerical information to input. + default: default value + minval: minimum value for imput + maxval: maximum value for input + + The number input must be in the range minval .. maxval if these are + given. If not, a hint is issued and the dialog remains open for + correction. Return the number input. + If the dialog is canceled, return None. + + Example (for a TurtleScreen instance named screen): + >>> screen.numinput("Poker", "Your stakes:", 1000, minval=10, maxval=10000) + + """ + return simpledialog.askfloat(title, prompt, initialvalue=default, + minvalue=minval, maxvalue=maxval) + + +############################################################################## +### End of Tkinter - interface ### +############################################################################## + + +class Terminator (Exception): + """Will be raised in TurtleScreen.update, if _RUNNING becomes False. + + This stops execution of a turtle graphics script. + Main purpose: use in the Demo-Viewer turtle.Demo.py. + """ + pass + + +class TurtleGraphicsError(Exception): + """Some TurtleGraphics Error + """ + + +class Shape(object): + """Data structure modeling shapes. + + attribute _type is one of "polygon", "image", "compound" + attribute _data is - depending on _type a poygon-tuple, + an image or a list constructed using the addcomponent method. + """ + def __init__(self, type_, data=None): + self._type = type_ + if type_ == "polygon": + if isinstance(data, list): + data = tuple(data) + elif type_ == "image": + if isinstance(data, str): + if data.lower().endswith(".gif") and isfile(data): + data = TurtleScreen._image(data) + # else data assumed to be Photoimage + elif type_ == "compound": + data = [] + else: + raise TurtleGraphicsError("There is no shape type %s" % type_) + self._data = data + + def addcomponent(self, poly, fill, outline=None): + """Add component to a shape of type compound. + + Arguments: poly is a polygon, i. e. a tuple of number pairs. + fill is the fillcolor of the component, + outline is the outline color of the component. + + call (for a Shapeobject namend s): + -- s.addcomponent(((0,0), (10,10), (-10,10)), "red", "blue") + + Example: + >>> poly = ((0,0),(10,-5),(0,10),(-10,-5)) + >>> s = Shape("compound") + >>> s.addcomponent(poly, "red", "blue") + >>> # .. add more components and then use register_shape() + """ + if self._type != "compound": + raise TurtleGraphicsError("Cannot add component to %s Shape" + % self._type) + if outline is None: + outline = fill + self._data.append([poly, fill, outline]) + + +class Tbuffer(object): + """Ring buffer used as undobuffer for RawTurtle objects.""" + def __init__(self, bufsize=10): + self.bufsize = bufsize + self.buffer = [[None]] * bufsize + self.ptr = -1 + self.cumulate = False + def reset(self, bufsize=None): + if bufsize is None: + for i in range(self.bufsize): + self.buffer[i] = [None] + else: + self.bufsize = bufsize + self.buffer = [[None]] * bufsize + self.ptr = -1 + def push(self, item): + if self.bufsize > 0: + if not self.cumulate: + self.ptr = (self.ptr + 1) % self.bufsize + self.buffer[self.ptr] = item + else: + self.buffer[self.ptr].append(item) + def pop(self): + if self.bufsize > 0: + item = self.buffer[self.ptr] + if item is None: + return None + else: + self.buffer[self.ptr] = [None] + self.ptr = (self.ptr - 1) % self.bufsize + return (item) + def nr_of_items(self): + return self.bufsize - self.buffer.count([None]) + def __repr__(self): + return str(self.buffer) + " " + str(self.ptr) + + + +class TurtleScreen(TurtleScreenBase): + """Provides screen oriented methods like setbg etc. + + Only relies upon the methods of TurtleScreenBase and NOT + upon components of the underlying graphics toolkit - + which is Tkinter in this case. + """ + _RUNNING = True + + def __init__(self, cv, mode=_CFG["mode"], + colormode=_CFG["colormode"], delay=_CFG["delay"]): + self._shapes = { + "arrow" : Shape("polygon", ((-10,0), (10,0), (0,10))), + "turtle" : Shape("polygon", ((0,16), (-2,14), (-1,10), (-4,7), + (-7,9), (-9,8), (-6,5), (-7,1), (-5,-3), (-8,-6), + (-6,-8), (-4,-5), (0,-7), (4,-5), (6,-8), (8,-6), + (5,-3), (7,1), (6,5), (9,8), (7,9), (4,7), (1,10), + (2,14))), + "circle" : Shape("polygon", ((10,0), (9.51,3.09), (8.09,5.88), + (5.88,8.09), (3.09,9.51), (0,10), (-3.09,9.51), + (-5.88,8.09), (-8.09,5.88), (-9.51,3.09), (-10,0), + (-9.51,-3.09), (-8.09,-5.88), (-5.88,-8.09), + (-3.09,-9.51), (-0.00,-10.00), (3.09,-9.51), + (5.88,-8.09), (8.09,-5.88), (9.51,-3.09))), + "square" : Shape("polygon", ((10,-10), (10,10), (-10,10), + (-10,-10))), + "triangle" : Shape("polygon", ((10,-5.77), (0,11.55), + (-10,-5.77))), + "classic": Shape("polygon", ((0,0),(-5,-9),(0,-7),(5,-9))), + "blank" : Shape("image", self._blankimage()) + } + + self._bgpics = {"nopic" : ""} + + TurtleScreenBase.__init__(self, cv) + self._mode = mode + self._delayvalue = delay + self._colormode = _CFG["colormode"] + self._keys = [] + self.clear() + if 0 and sys.platform == 'darwin': # disabled for webpython + # Force Turtle window to the front on OS X. This is needed because + # the Turtle window will show behind the Terminal window when you + # start the demo from the command line. + cv._rootwindow.call('wm', 'attributes', '.', '-topmost', '1') + cv._rootwindow.call('wm', 'attributes', '.', '-topmost', '0') + + def clear(self): + """Delete all drawings and all turtles from the TurtleScreen. + + No argument. + + Reset empty TurtleScreen to its initial state: white background, + no backgroundimage, no eventbindings and tracing on. + + Example (for a TurtleScreen instance named screen): + >>> screen.clear() + + Note: this method is not available as function. + """ + self._delayvalue = _CFG["delay"] + self._colormode = _CFG["colormode"] + self._delete("all") + self._bgpic = self._createimage("") + self._bgpicname = "nopic" + self._tracing = 1 + self._updatecounter = 0 + self._turtles = [] + self.bgcolor("white") + for btn in 1, 2, 3: + self.onclick(None, btn) + self.onkeypress(None) + for key in self._keys[:]: + self.onkey(None, key) + self.onkeypress(None, key) + Turtle._pen = None + + def mode(self, mode=None): + """Set turtle-mode ('standard', 'logo' or 'world') and perform reset. + + Optional argument: + mode -- on of the strings 'standard', 'logo' or 'world' + + Mode 'standard' is compatible with turtle.py. + Mode 'logo' is compatible with most Logo-Turtle-Graphics. + Mode 'world' uses userdefined 'worldcoordinates'. *Attention*: in + this mode angles appear distorted if x/y unit-ratio doesn't equal 1. + If mode is not given, return the current mode. + + Mode Initial turtle heading positive angles + ------------|-------------------------|------------------- + 'standard' to the right (east) counterclockwise + 'logo' upward (north) clockwise + + Examples: + >>> mode('logo') # resets turtle heading to north + >>> mode() + 'logo' + """ + if mode is None: + return self._mode + mode = mode.lower() + if mode not in ["standard", "logo", "world"]: + raise TurtleGraphicsError("No turtle-graphics-mode %s" % mode) + self._mode = mode + if mode in ["standard", "logo"]: + self._setscrollregion(-self.canvwidth//2, -self.canvheight//2, + self.canvwidth//2, self.canvheight//2) + self.xscale = self.yscale = 1.0 + self.reset() + + def setworldcoordinates(self, llx, lly, urx, ury): + """Set up a user defined coordinate-system. + + Arguments: + llx -- a number, x-coordinate of lower left corner of canvas + lly -- a number, y-coordinate of lower left corner of canvas + urx -- a number, x-coordinate of upper right corner of canvas + ury -- a number, y-coordinate of upper right corner of canvas + + Set up user coodinat-system and switch to mode 'world' if necessary. + This performs a screen.reset. If mode 'world' is already active, + all drawings are redrawn according to the new coordinates. + + But ATTENTION: in user-defined coordinatesystems angles may appear + distorted. (see Screen.mode()) + + Example (for a TurtleScreen instance named screen): + >>> screen.setworldcoordinates(-10,-0.5,50,1.5) + >>> for _ in range(36): + ... left(10) + ... forward(0.5) + """ + if self.mode() != "world": + self.mode("world") + xspan = float(urx - llx) + yspan = float(ury - lly) + wx, wy = self._window_size() + self.screensize(wx-20, wy-20) + oldxscale, oldyscale = self.xscale, self.yscale + self.xscale = self.canvwidth / xspan + self.yscale = self.canvheight / yspan + srx1 = llx * self.xscale + sry1 = -ury * self.yscale + srx2 = self.canvwidth + srx1 + sry2 = self.canvheight + sry1 + self._setscrollregion(srx1, sry1, srx2, sry2) + self._rescale(self.xscale/oldxscale, self.yscale/oldyscale) + self.update() + + def register_shape(self, name, shape=None): + """Adds a turtle shape to TurtleScreen's shapelist. + + Arguments: + (1) name is the name of a gif-file and shape is None. + Installs the corresponding image shape. + !! Image-shapes DO NOT rotate when turning the turtle, + !! so they do not display the heading of the turtle! + (2) name is an arbitrary string and shape is a tuple + of pairs of coordinates. Installs the corresponding + polygon shape + (3) name is an arbitrary string and shape is a + (compound) Shape object. Installs the corresponding + compound shape. + To use a shape, you have to issue the command shape(shapename). + + call: register_shape("turtle.gif") + --or: register_shape("tri", ((0,0), (10,10), (-10,10))) + + Example (for a TurtleScreen instance named screen): + >>> screen.register_shape("triangle", ((5,-3),(0,5),(-5,-3))) + + """ + if shape is None: + # image + if name.lower().endswith(".gif"): + shape = Shape("image", self._image(name)) + else: + raise TurtleGraphicsError("Bad arguments for register_shape.\n" + + "Use help(register_shape)" ) + elif isinstance(shape, tuple): + shape = Shape("polygon", shape) + ## else shape assumed to be Shape-instance + self._shapes[name] = shape + + def _colorstr(self, color): + """Return color string corresponding to args. + + Argument may be a string or a tuple of three + numbers corresponding to actual colormode, + i.e. in the range 0<=n<=colormode. + + If the argument doesn't represent a color, + an error is raised. + """ + if len(color) == 1: + color = color[0] + if isinstance(color, str): + if self._iscolorstring(color) or color == "": + return color + else: + raise TurtleGraphicsError("bad color string: %s" % str(color)) + try: + r, g, b = color + except: + raise TurtleGraphicsError("bad color arguments: %s" % str(color)) + if self._colormode == 1.0: + r, g, b = [round(255.0*x) for x in (r, g, b)] + if not ((0 <= r <= 255) and (0 <= g <= 255) and (0 <= b <= 255)): + raise TurtleGraphicsError("bad color sequence: %s" % str(color)) + return "#%02x%02x%02x" % (r, g, b) + + def _color(self, cstr): + if not cstr.startswith("#"): + return cstr + if len(cstr) == 7: + cl = [int(cstr[i:i+2], 16) for i in (1, 3, 5)] + elif len(cstr) == 4: + cl = [16*int(cstr[h], 16) for h in cstr[1:]] + else: + raise TurtleGraphicsError("bad colorstring: %s" % cstr) + return tuple([c * self._colormode/255 for c in cl]) + + def colormode(self, cmode=None): + """Return the colormode or set it to 1.0 or 255. + + Optional argument: + cmode -- one of the values 1.0 or 255 + + r, g, b values of colortriples have to be in range 0..cmode. + + Example (for a TurtleScreen instance named screen): + >>> screen.colormode() + 1.0 + >>> screen.colormode(255) + >>> pencolor(240,160,80) + """ + if cmode is None: + return self._colormode + if cmode == 1.0: + self._colormode = float(cmode) + elif cmode == 255: + self._colormode = int(cmode) + + def reset(self): + """Reset all Turtles on the Screen to their initial state. + + No argument. + + Example (for a TurtleScreen instance named screen): + >>> screen.reset() + """ + for turtle in self._turtles: + turtle._setmode(self._mode) + turtle.reset() + + def turtles(self): + """Return the list of turtles on the screen. + + Example (for a TurtleScreen instance named screen): + >>> screen.turtles() + [] + """ + return self._turtles + + def bgcolor(self, *args): + """Set or return backgroundcolor of the TurtleScreen. + + Arguments (if given): a color string or three numbers + in the range 0..colormode or a 3-tuple of such numbers. + + Example (for a TurtleScreen instance named screen): + >>> screen.bgcolor("orange") + >>> screen.bgcolor() + 'orange' + >>> screen.bgcolor(0.5,0,0.5) + >>> screen.bgcolor() + '#800080' + """ + if args: + color = self._colorstr(args) + else: + color = None + color = self._bgcolor(color) + if color is not None: + color = self._color(color) + return color + + def tracer(self, n=None, delay=None): + """Turns turtle animation on/off and set delay for update drawings. + + Optional arguments: + n -- nonnegative integer + delay -- nonnegative integer + + If n is given, only each n-th regular screen update is really performed. + (Can be used to accelerate the drawing of complex graphics.) + Second arguments sets delay value (see RawTurtle.delay()) + + Example (for a TurtleScreen instance named screen): + >>> screen.tracer(8, 25) + >>> dist = 2 + >>> for i in range(200): + ... fd(dist) + ... rt(90) + ... dist += 2 + """ + if n is None: + return self._tracing + self._tracing = int(n) + self._updatecounter = 0 + if delay is not None: + self._delayvalue = int(delay) + if self._tracing: + self.update() + + def delay(self, delay=None): + """ Return or set the drawing delay in milliseconds. + + Optional argument: + delay -- positive integer + + Example (for a TurtleScreen instance named screen): + >>> screen.delay(15) + >>> screen.delay() + 15 + """ + if delay is None: + return self._delayvalue + self._delayvalue = int(delay) + + def _incrementudc(self): + """Increment update counter.""" + if not TurtleScreen._RUNNING: + TurtleScreen._RUNNNING = True + raise Terminator + if self._tracing > 0: + self._updatecounter += 1 + self._updatecounter %= self._tracing + + def update(self): + """Perform a TurtleScreen update. + """ + tracing = self._tracing + self._tracing = True + for t in self.turtles(): + t._update_data() + t._drawturtle() + self._tracing = tracing + self._update() + + def window_width(self): + """ Return the width of the turtle window. + + Example (for a TurtleScreen instance named screen): + >>> screen.window_width() + 640 + """ + return self._window_size()[0] + + def window_height(self): + """ Return the height of the turtle window. + + Example (for a TurtleScreen instance named screen): + >>> screen.window_height() + 480 + """ + return self._window_size()[1] + + def getcanvas(self): + """Return the Canvas of this TurtleScreen. + + No argument. + + Example (for a Screen instance named screen): + >>> cv = screen.getcanvas() + >>> cv + + """ + return self.cv + + def getshapes(self): + """Return a list of names of all currently available turtle shapes. + + No argument. + + Example (for a TurtleScreen instance named screen): + >>> screen.getshapes() + ['arrow', 'blank', 'circle', ... , 'turtle'] + """ + return sorted(self._shapes.keys()) + + def onclick(self, fun, btn=1, add=None): + """Bind fun to mouse-click event on canvas. + + Arguments: + fun -- a function with two arguments, the coordinates of the + clicked point on the canvas. + num -- the number of the mouse-button, defaults to 1 + + Example (for a TurtleScreen instance named screen) + + >>> screen.onclick(goto) + >>> # Subsequently clicking into the TurtleScreen will + >>> # make the turtle move to the clicked point. + >>> screen.onclick(None) + """ + self._onscreenclick(fun, btn, add) + + def onkey(self, fun, key): + """Bind fun to key-release event of key. + + Arguments: + fun -- a function with no arguments + key -- a string: key (e.g. "a") or key-symbol (e.g. "space") + + In order to be able to register key-events, TurtleScreen + must have focus. (See method listen.) + + Example (for a TurtleScreen instance named screen): + + >>> def f(): + ... fd(50) + ... lt(60) + ... + >>> screen.onkey(f, "Up") + >>> screen.listen() + + Subsequently the turtle can be moved by repeatedly pressing + the up-arrow key, consequently drawing a hexagon + + """ + if fun is None: + if key in self._keys: + self._keys.remove(key) + elif key not in self._keys: + self._keys.append(key) + self._onkeyrelease(fun, key) + + def onkeypress(self, fun, key=None): + """Bind fun to key-press event of key if key is given, + or to any key-press-event if no key is given. + + Arguments: + fun -- a function with no arguments + key -- a string: key (e.g. "a") or key-symbol (e.g. "space") + + In order to be able to register key-events, TurtleScreen + must have focus. (See method listen.) + + Example (for a TurtleScreen instance named screen + and a Turtle instance named turtle): + + >>> def f(): + ... fd(50) + ... lt(60) + ... + >>> screen.onkeypress(f, "Up") + >>> screen.listen() + + Subsequently the turtle can be moved by repeatedly pressing + the up-arrow key, or by keeping pressed the up-arrow key. + consequently drawing a hexagon. + """ + if fun is None: + if key in self._keys: + self._keys.remove(key) + elif key is not None and key not in self._keys: + self._keys.append(key) + self._onkeypress(fun, key) + + def listen(self, xdummy=None, ydummy=None): + """Set focus on TurtleScreen (in order to collect key-events) + + No arguments. + Dummy arguments are provided in order + to be able to pass listen to the onclick method. + + Example (for a TurtleScreen instance named screen): + >>> screen.listen() + """ + self._listen() + + def ontimer(self, fun, t=0): + """Install a timer, which calls fun after t milliseconds. + + Arguments: + fun -- a function with no arguments. + t -- a number >= 0 + + Example (for a TurtleScreen instance named screen): + + >>> running = True + >>> def f(): + ... if running: + ... fd(50) + ... lt(60) + ... screen.ontimer(f, 250) + ... + >>> f() # makes the turtle marching around + >>> running = False + """ + self._ontimer(fun, t) + + def bgpic(self, picname=None): + """Set background image or return name of current backgroundimage. + + Optional argument: + picname -- a string, name of a gif-file or "nopic". + + If picname is a filename, set the corresponding image as background. + If picname is "nopic", delete backgroundimage, if present. + If picname is None, return the filename of the current backgroundimage. + + Example (for a TurtleScreen instance named screen): + >>> screen.bgpic() + 'nopic' + >>> screen.bgpic("landscape.gif") + >>> screen.bgpic() + 'landscape.gif' + """ + if picname is None: + return self._bgpicname + if picname not in self._bgpics: + self._bgpics[picname] = self._image(picname) + self._setbgpic(self._bgpic, self._bgpics[picname]) + self._bgpicname = picname + + def screensize(self, canvwidth=None, canvheight=None, bg=None): + """Resize the canvas the turtles are drawing on. + + Optional arguments: + canvwidth -- positive integer, new width of canvas in pixels + canvheight -- positive integer, new height of canvas in pixels + bg -- colorstring or color-tuple, new backgroundcolor + If no arguments are given, return current (canvaswidth, canvasheight) + + Do not alter the drawing window. To observe hidden parts of + the canvas use the scrollbars. (Can make visible those parts + of a drawing, which were outside the canvas before!) + + Example (for a Turtle instance named turtle): + >>> turtle.screensize(2000,1500) + >>> # e.g. to search for an erroneously escaped turtle ;-) + """ + return self._resize(canvwidth, canvheight, bg) + + onscreenclick = onclick + resetscreen = reset + clearscreen = clear + addshape = register_shape + onkeyrelease = onkey + +class TNavigator(object): + """Navigation part of the RawTurtle. + Implements methods for turtle movement. + """ + START_ORIENTATION = { + "standard": Vec2D(1.0, 0.0), + "world" : Vec2D(1.0, 0.0), + "logo" : Vec2D(0.0, 1.0) } + DEFAULT_MODE = "standard" + DEFAULT_ANGLEOFFSET = 0 + DEFAULT_ANGLEORIENT = 1 + + def __init__(self, mode=DEFAULT_MODE): + self._angleOffset = self.DEFAULT_ANGLEOFFSET + self._angleOrient = self.DEFAULT_ANGLEORIENT + self._mode = mode + self.undobuffer = None + self.degrees() + self._mode = None + self._setmode(mode) + TNavigator.reset(self) + + def reset(self): + """reset turtle to its initial values + + Will be overwritten by parent class + """ + self._position = Vec2D(0.0, 0.0) + self._orient = TNavigator.START_ORIENTATION[self._mode] + + def _setmode(self, mode=None): + """Set turtle-mode to 'standard', 'world' or 'logo'. + """ + if mode is None: + return self._mode + if mode not in ["standard", "logo", "world"]: + return + self._mode = mode + if mode in ["standard", "world"]: + self._angleOffset = 0 + self._angleOrient = 1 + else: # mode == "logo": + self._angleOffset = self._fullcircle/4. + self._angleOrient = -1 + + def _setDegreesPerAU(self, fullcircle): + """Helper function for degrees() and radians()""" + self._fullcircle = fullcircle + self._degreesPerAU = 360/fullcircle + if self._mode == "standard": + self._angleOffset = 0 + else: + self._angleOffset = fullcircle/4. + + def degrees(self, fullcircle=360.0): + """ Set angle measurement units to degrees. + + Optional argument: + fullcircle - a number + + Set angle measurement units, i. e. set number + of 'degrees' for a full circle. Dafault value is + 360 degrees. + + Example (for a Turtle instance named turtle): + >>> turtle.left(90) + >>> turtle.heading() + 90 + + Change angle measurement unit to grad (also known as gon, + grade, or gradian and equals 1/100-th of the right angle.) + >>> turtle.degrees(400.0) + >>> turtle.heading() + 100 + + """ + self._setDegreesPerAU(fullcircle) + + def radians(self): + """ Set the angle measurement units to radians. + + No arguments. + + Example (for a Turtle instance named turtle): + >>> turtle.heading() + 90 + >>> turtle.radians() + >>> turtle.heading() + 1.5707963267948966 + """ + self._setDegreesPerAU(2*math.pi) + + def _go(self, distance): + """move turtle forward by specified distance""" + ende = self._position + self._orient * distance + self._goto(ende) + + def _rotate(self, angle): + """Turn turtle counterclockwise by specified angle if angle > 0.""" + angle *= self._degreesPerAU + self._orient = self._orient.rotate(angle) + + def _goto(self, end): + """move turtle to position end.""" + self._position = end + + def forward(self, distance): + """Move the turtle forward by the specified distance. + + Aliases: forward | fd + + Argument: + distance -- a number (integer or float) + + Move the turtle forward by the specified distance, in the direction + the turtle is headed. + + Example (for a Turtle instance named turtle): + >>> turtle.position() + (0.00, 0.00) + >>> turtle.forward(25) + >>> turtle.position() + (25.00,0.00) + >>> turtle.forward(-75) + >>> turtle.position() + (-50.00,0.00) + """ + self._go(distance) + + def back(self, distance): + """Move the turtle backward by distance. + + Aliases: back | backward | bk + + Argument: + distance -- a number + + Move the turtle backward by distance ,opposite to the direction the + turtle is headed. Do not change the turtle's heading. + + Example (for a Turtle instance named turtle): + >>> turtle.position() + (0.00, 0.00) + >>> turtle.backward(30) + >>> turtle.position() + (-30.00, 0.00) + """ + self._go(-distance) + + def right(self, angle): + """Turn turtle right by angle units. + + Aliases: right | rt + + Argument: + angle -- a number (integer or float) + + Turn turtle right by angle units. (Units are by default degrees, + but can be set via the degrees() and radians() functions.) + Angle orientation depends on mode. (See this.) + + Example (for a Turtle instance named turtle): + >>> turtle.heading() + 22.0 + >>> turtle.right(45) + >>> turtle.heading() + 337.0 + """ + self._rotate(-angle) + + def left(self, angle): + """Turn turtle left by angle units. + + Aliases: left | lt + + Argument: + angle -- a number (integer or float) + + Turn turtle left by angle units. (Units are by default degrees, + but can be set via the degrees() and radians() functions.) + Angle orientation depends on mode. (See this.) + + Example (for a Turtle instance named turtle): + >>> turtle.heading() + 22.0 + >>> turtle.left(45) + >>> turtle.heading() + 67.0 + """ + self._rotate(angle) + + def pos(self): + """Return the turtle's current location (x,y), as a Vec2D-vector. + + Aliases: pos | position + + No arguments. + + Example (for a Turtle instance named turtle): + >>> turtle.pos() + (0.00, 240.00) + """ + return self._position + + def xcor(self): + """ Return the turtle's x coordinate. + + No arguments. + + Example (for a Turtle instance named turtle): + >>> reset() + >>> turtle.left(60) + >>> turtle.forward(100) + >>> print turtle.xcor() + 50.0 + """ + return self._position[0] + + def ycor(self): + """ Return the turtle's y coordinate + --- + No arguments. + + Example (for a Turtle instance named turtle): + >>> reset() + >>> turtle.left(60) + >>> turtle.forward(100) + >>> print turtle.ycor() + 86.6025403784 + """ + return self._position[1] + + + def goto(self, x, y=None): + """Move turtle to an absolute position. + + Aliases: setpos | setposition | goto: + + Arguments: + x -- a number or a pair/vector of numbers + y -- a number None + + call: goto(x, y) # two coordinates + --or: goto((x, y)) # a pair (tuple) of coordinates + --or: goto(vec) # e.g. as returned by pos() + + Move turtle to an absolute position. If the pen is down, + a line will be drawn. The turtle's orientation does not change. + + Example (for a Turtle instance named turtle): + >>> tp = turtle.pos() + >>> tp + (0.00, 0.00) + >>> turtle.setpos(60,30) + >>> turtle.pos() + (60.00,30.00) + >>> turtle.setpos((20,80)) + >>> turtle.pos() + (20.00,80.00) + >>> turtle.setpos(tp) + >>> turtle.pos() + (0.00,0.00) + """ + if y is None: + self._goto(Vec2D(*x)) + else: + self._goto(Vec2D(x, y)) + + def home(self): + """Move turtle to the origin - coordinates (0,0). + + No arguments. + + Move turtle to the origin - coordinates (0,0) and set its + heading to its start-orientation (which depends on mode). + + Example (for a Turtle instance named turtle): + >>> turtle.home() + """ + self.goto(0, 0) + self.setheading(0) + + def setx(self, x): + """Set the turtle's first coordinate to x + + Argument: + x -- a number (integer or float) + + Set the turtle's first coordinate to x, leave second coordinate + unchanged. + + Example (for a Turtle instance named turtle): + >>> turtle.position() + (0.00, 240.00) + >>> turtle.setx(10) + >>> turtle.position() + (10.00, 240.00) + """ + self._goto(Vec2D(x, self._position[1])) + + def sety(self, y): + """Set the turtle's second coordinate to y + + Argument: + y -- a number (integer or float) + + Set the turtle's first coordinate to x, second coordinate remains + unchanged. + + Example (for a Turtle instance named turtle): + >>> turtle.position() + (0.00, 40.00) + >>> turtle.sety(-10) + >>> turtle.position() + (0.00, -10.00) + """ + self._goto(Vec2D(self._position[0], y)) + + def distance(self, x, y=None): + """Return the distance from the turtle to (x,y) in turtle step units. + + Arguments: + x -- a number or a pair/vector of numbers or a turtle instance + y -- a number None None + + call: distance(x, y) # two coordinates + --or: distance((x, y)) # a pair (tuple) of coordinates + --or: distance(vec) # e.g. as returned by pos() + --or: distance(mypen) # where mypen is another turtle + + Example (for a Turtle instance named turtle): + >>> turtle.pos() + (0.00, 0.00) + >>> turtle.distance(30,40) + 50.0 + >>> pen = Turtle() + >>> pen.forward(77) + >>> turtle.distance(pen) + 77.0 + """ + if y is not None: + pos = Vec2D(x, y) + if isinstance(x, Vec2D): + pos = x + elif isinstance(x, tuple): + pos = Vec2D(*x) + elif isinstance(x, TNavigator): + pos = x._position + return abs(pos - self._position) + + def towards(self, x, y=None): + """Return the angle of the line from the turtle's position to (x, y). + + Arguments: + x -- a number or a pair/vector of numbers or a turtle instance + y -- a number None None + + call: distance(x, y) # two coordinates + --or: distance((x, y)) # a pair (tuple) of coordinates + --or: distance(vec) # e.g. as returned by pos() + --or: distance(mypen) # where mypen is another turtle + + Return the angle, between the line from turtle-position to position + specified by x, y and the turtle's start orientation. (Depends on + modes - "standard" or "logo") + + Example (for a Turtle instance named turtle): + >>> turtle.pos() + (10.00, 10.00) + >>> turtle.towards(0,0) + 225.0 + """ + if y is not None: + pos = Vec2D(x, y) + if isinstance(x, Vec2D): + pos = x + elif isinstance(x, tuple): + pos = Vec2D(*x) + elif isinstance(x, TNavigator): + pos = x._position + x, y = pos - self._position + result = round(math.atan2(y, x)*180.0/math.pi, 10) % 360.0 + result /= self._degreesPerAU + return (self._angleOffset + self._angleOrient*result) % self._fullcircle + + def heading(self): + """ Return the turtle's current heading. + + No arguments. + + Example (for a Turtle instance named turtle): + >>> turtle.left(67) + >>> turtle.heading() + 67.0 + """ + x, y = self._orient + result = round(math.atan2(y, x)*180.0/math.pi, 10) % 360.0 + result /= self._degreesPerAU + return (self._angleOffset + self._angleOrient*result) % self._fullcircle + + def setheading(self, to_angle): + """Set the orientation of the turtle to to_angle. + + Aliases: setheading | seth + + Argument: + to_angle -- a number (integer or float) + + Set the orientation of the turtle to to_angle. + Here are some common directions in degrees: + + standard - mode: logo-mode: + -------------------|-------------------- + 0 - east 0 - north + 90 - north 90 - east + 180 - west 180 - south + 270 - south 270 - west + + Example (for a Turtle instance named turtle): + >>> turtle.setheading(90) + >>> turtle.heading() + 90 + """ + angle = (to_angle - self.heading())*self._angleOrient + full = self._fullcircle + angle = (angle+full/2.)%full - full/2. + self._rotate(angle) + + def circle(self, radius, extent = None, steps = None): + """ Draw a circle with given radius. + + Arguments: + radius -- a number + extent (optional) -- a number + steps (optional) -- an integer + + Draw a circle with given radius. The center is radius units left + of the turtle; extent - an angle - determines which part of the + circle is drawn. If extent is not given, draw the entire circle. + If extent is not a full circle, one endpoint of the arc is the + current pen position. Draw the arc in counterclockwise direction + if radius is positive, otherwise in clockwise direction. Finally + the direction of the turtle is changed by the amount of extent. + + As the circle is approximated by an inscribed regular polygon, + steps determines the number of steps to use. If not given, + it will be calculated automatically. Maybe used to draw regular + polygons. + + call: circle(radius) # full circle + --or: circle(radius, extent) # arc + --or: circle(radius, extent, steps) + --or: circle(radius, steps=6) # 6-sided polygon + + Example (for a Turtle instance named turtle): + >>> turtle.circle(50) + >>> turtle.circle(120, 180) # semicircle + """ + if self.undobuffer: + self.undobuffer.push(["seq"]) + self.undobuffer.cumulate = True + speed = self.speed() + if extent is None: + extent = self._fullcircle + if steps is None: + frac = abs(extent)/self._fullcircle + steps = 1+int(min(11+abs(radius)/6.0, 59.0)*frac) + w = 1.0 * extent / steps + w2 = 0.5 * w + l = 2.0 * radius * math.sin(w2*math.pi/180.0*self._degreesPerAU) + if radius < 0: + l, w, w2 = -l, -w, -w2 + tr = self._tracer() + dl = self._delay() + if speed == 0: + self._tracer(0, 0) + else: + self.speed(0) + self._rotate(w2) + for i in range(steps): + self.speed(speed) + self._go(l) + self.speed(0) + self._rotate(w) + self._rotate(-w2) + if speed == 0: + self._tracer(tr, dl) + self.speed(speed) + if self.undobuffer: + self.undobuffer.cumulate = False + +## three dummy methods to be implemented by child class: + + def speed(self, s=0): + """dummy method - to be overwritten by child class""" + def _tracer(self, a=None, b=None): + """dummy method - to be overwritten by child class""" + def _delay(self, n=None): + """dummy method - to be overwritten by child class""" + + fd = forward + bk = back + backward = back + rt = right + lt = left + position = pos + setpos = goto + setposition = goto + seth = setheading + + +class TPen(object): + """Drawing part of the RawTurtle. + Implements drawing properties. + """ + def __init__(self, resizemode=_CFG["resizemode"]): + self._resizemode = resizemode # or "user" or "noresize" + self.undobuffer = None + TPen._reset(self) + + def _reset(self, pencolor=_CFG["pencolor"], + fillcolor=_CFG["fillcolor"]): + self._pensize = 1 + self._shown = True + self._pencolor = pencolor + self._fillcolor = fillcolor + self._drawing = True + self._speed = 3 + self._stretchfactor = (1., 1.) + self._shearfactor = 0. + self._tilt = 0. + self._shapetrafo = (1., 0., 0., 1.) + self._outlinewidth = 1 + + def resizemode(self, rmode=None): + """Set resizemode to one of the values: "auto", "user", "noresize". + + (Optional) Argument: + rmode -- one of the strings "auto", "user", "noresize" + + Different resizemodes have the following effects: + - "auto" adapts the appearance of the turtle + corresponding to the value of pensize. + - "user" adapts the appearance of the turtle according to the + values of stretchfactor and outlinewidth (outline), + which are set by shapesize() + - "noresize" no adaption of the turtle's appearance takes place. + If no argument is given, return current resizemode. + resizemode("user") is called by a call of shapesize with arguments. + + + Examples (for a Turtle instance named turtle): + >>> turtle.resizemode("noresize") + >>> turtle.resizemode() + 'noresize' + """ + if rmode is None: + return self._resizemode + rmode = rmode.lower() + if rmode in ["auto", "user", "noresize"]: + self.pen(resizemode=rmode) + + def pensize(self, width=None): + """Set or return the line thickness. + + Aliases: pensize | width + + Argument: + width -- positive number + + Set the line thickness to width or return it. If resizemode is set + to "auto" and turtleshape is a polygon, that polygon is drawn with + the same line thickness. If no argument is given, current pensize + is returned. + + Example (for a Turtle instance named turtle): + >>> turtle.pensize() + 1 + >>> turtle.pensize(10) # from here on lines of width 10 are drawn + """ + if width is None: + return self._pensize + self.pen(pensize=width) + + + def penup(self): + """Pull the pen up -- no drawing when moving. + + Aliases: penup | pu | up + + No argument + + Example (for a Turtle instance named turtle): + >>> turtle.penup() + """ + if not self._drawing: + return + self.pen(pendown=False) + + def pendown(self): + """Pull the pen down -- drawing when moving. + + Aliases: pendown | pd | down + + No argument. + + Example (for a Turtle instance named turtle): + >>> turtle.pendown() + """ + if self._drawing: + return + self.pen(pendown=True) + + def isdown(self): + """Return True if pen is down, False if it's up. + + No argument. + + Example (for a Turtle instance named turtle): + >>> turtle.penup() + >>> turtle.isdown() + False + >>> turtle.pendown() + >>> turtle.isdown() + True + """ + return self._drawing + + def speed(self, speed=None): + """ Return or set the turtle's speed. + + Optional argument: + speed -- an integer in the range 0..10 or a speedstring (see below) + + Set the turtle's speed to an integer value in the range 0 .. 10. + If no argument is given: return current speed. + + If input is a number greater than 10 or smaller than 0.5, + speed is set to 0. + Speedstrings are mapped to speedvalues in the following way: + 'fastest' : 0 + 'fast' : 10 + 'normal' : 6 + 'slow' : 3 + 'slowest' : 1 + speeds from 1 to 10 enforce increasingly faster animation of + line drawing and turtle turning. + + Attention: + speed = 0 : *no* animation takes place. forward/back makes turtle jump + and likewise left/right make the turtle turn instantly. + + Example (for a Turtle instance named turtle): + >>> turtle.speed(3) + """ + speeds = {'fastest':0, 'fast':10, 'normal':6, 'slow':3, 'slowest':1 } + if speed is None: + return self._speed + if speed in speeds: + speed = speeds[speed] + elif 0.5 < speed < 10.5: + speed = int(round(speed)) + else: + speed = 0 + self.pen(speed=speed) + + def color(self, *args): + """Return or set the pencolor and fillcolor. + + Arguments: + Several input formats are allowed. + They use 0, 1, 2, or 3 arguments as follows: + + color() + Return the current pencolor and the current fillcolor + as a pair of color specification strings as are returned + by pencolor and fillcolor. + color(colorstring), color((r,g,b)), color(r,g,b) + inputs as in pencolor, set both, fillcolor and pencolor, + to the given value. + color(colorstring1, colorstring2), + color((r1,g1,b1), (r2,g2,b2)) + equivalent to pencolor(colorstring1) and fillcolor(colorstring2) + and analogously, if the other input format is used. + + If turtleshape is a polygon, outline and interior of that polygon + is drawn with the newly set colors. + For mor info see: pencolor, fillcolor + + Example (for a Turtle instance named turtle): + >>> turtle.color('red', 'green') + >>> turtle.color() + ('red', 'green') + >>> colormode(255) + >>> color((40, 80, 120), (160, 200, 240)) + >>> color() + ('#285078', '#a0c8f0') + """ + if args: + l = len(args) + if l == 1: + pcolor = fcolor = args[0] + elif l == 2: + pcolor, fcolor = args + elif l == 3: + pcolor = fcolor = args + pcolor = self._colorstr(pcolor) + fcolor = self._colorstr(fcolor) + self.pen(pencolor=pcolor, fillcolor=fcolor) + else: + return self._color(self._pencolor), self._color(self._fillcolor) + + def pencolor(self, *args): + """ Return or set the pencolor. + + Arguments: + Four input formats are allowed: + - pencolor() + Return the current pencolor as color specification string, + possibly in hex-number format (see example). + May be used as input to another color/pencolor/fillcolor call. + - pencolor(colorstring) + s is a Tk color specification string, such as "red" or "yellow" + - pencolor((r, g, b)) + *a tuple* of r, g, and b, which represent, an RGB color, + and each of r, g, and b are in the range 0..colormode, + where colormode is either 1.0 or 255 + - pencolor(r, g, b) + r, g, and b represent an RGB color, and each of r, g, and b + are in the range 0..colormode + + If turtleshape is a polygon, the outline of that polygon is drawn + with the newly set pencolor. + + Example (for a Turtle instance named turtle): + >>> turtle.pencolor('brown') + >>> tup = (0.2, 0.8, 0.55) + >>> turtle.pencolor(tup) + >>> turtle.pencolor() + '#33cc8c' + """ + if args: + color = self._colorstr(args) + if color == self._pencolor: + return + self.pen(pencolor=color) + else: + return self._color(self._pencolor) + + def fillcolor(self, *args): + """ Return or set the fillcolor. + + Arguments: + Four input formats are allowed: + - fillcolor() + Return the current fillcolor as color specification string, + possibly in hex-number format (see example). + May be used as input to another color/pencolor/fillcolor call. + - fillcolor(colorstring) + s is a Tk color specification string, such as "red" or "yellow" + - fillcolor((r, g, b)) + *a tuple* of r, g, and b, which represent, an RGB color, + and each of r, g, and b are in the range 0..colormode, + where colormode is either 1.0 or 255 + - fillcolor(r, g, b) + r, g, and b represent an RGB color, and each of r, g, and b + are in the range 0..colormode + + If turtleshape is a polygon, the interior of that polygon is drawn + with the newly set fillcolor. + + Example (for a Turtle instance named turtle): + >>> turtle.fillcolor('violet') + >>> col = turtle.pencolor() + >>> turtle.fillcolor(col) + >>> turtle.fillcolor(0, .5, 0) + """ + if args: + color = self._colorstr(args) + if color == self._fillcolor: + return + self.pen(fillcolor=color) + else: + return self._color(self._fillcolor) + + def showturtle(self): + """Makes the turtle visible. + + Aliases: showturtle | st + + No argument. + + Example (for a Turtle instance named turtle): + >>> turtle.hideturtle() + >>> turtle.showturtle() + """ + self.pen(shown=True) + + def hideturtle(self): + """Makes the turtle invisible. + + Aliases: hideturtle | ht + + No argument. + + It's a good idea to do this while you're in the + middle of a complicated drawing, because hiding + the turtle speeds up the drawing observably. + + Example (for a Turtle instance named turtle): + >>> turtle.hideturtle() + """ + self.pen(shown=False) + + def isvisible(self): + """Return True if the Turtle is shown, False if it's hidden. + + No argument. + + Example (for a Turtle instance named turtle): + >>> turtle.hideturtle() + >>> print turtle.isvisible(): + False + """ + return self._shown + + def pen(self, pen=None, **pendict): + """Return or set the pen's attributes. + + Arguments: + pen -- a dictionary with some or all of the below listed keys. + **pendict -- one or more keyword-arguments with the below + listed keys as keywords. + + Return or set the pen's attributes in a 'pen-dictionary' + with the following key/value pairs: + "shown" : True/False + "pendown" : True/False + "pencolor" : color-string or color-tuple + "fillcolor" : color-string or color-tuple + "pensize" : positive number + "speed" : number in range 0..10 + "resizemode" : "auto" or "user" or "noresize" + "stretchfactor": (positive number, positive number) + "shearfactor": number + "outline" : positive number + "tilt" : number + + This dictionary can be used as argument for a subsequent + pen()-call to restore the former pen-state. Moreover one + or more of these attributes can be provided as keyword-arguments. + This can be used to set several pen attributes in one statement. + + + Examples (for a Turtle instance named turtle): + >>> turtle.pen(fillcolor="black", pencolor="red", pensize=10) + >>> turtle.pen() + {'pensize': 10, 'shown': True, 'resizemode': 'auto', 'outline': 1, + 'pencolor': 'red', 'pendown': True, 'fillcolor': 'black', + 'stretchfactor': (1,1), 'speed': 3, 'shearfactor': 0.0} + >>> penstate=turtle.pen() + >>> turtle.color("yellow","") + >>> turtle.penup() + >>> turtle.pen() + {'pensize': 10, 'shown': True, 'resizemode': 'auto', 'outline': 1, + 'pencolor': 'yellow', 'pendown': False, 'fillcolor': '', + 'stretchfactor': (1,1), 'speed': 3, 'shearfactor': 0.0} + >>> p.pen(penstate, fillcolor="green") + >>> p.pen() + {'pensize': 10, 'shown': True, 'resizemode': 'auto', 'outline': 1, + 'pencolor': 'red', 'pendown': True, 'fillcolor': 'green', + 'stretchfactor': (1,1), 'speed': 3, 'shearfactor': 0.0} + """ + _pd = {"shown" : self._shown, + "pendown" : self._drawing, + "pencolor" : self._pencolor, + "fillcolor" : self._fillcolor, + "pensize" : self._pensize, + "speed" : self._speed, + "resizemode" : self._resizemode, + "stretchfactor" : self._stretchfactor, + "shearfactor" : self._shearfactor, + "outline" : self._outlinewidth, + "tilt" : self._tilt + } + + if not (pen or pendict): + return _pd + + if isinstance(pen, dict): + p = pen + else: + p = {} + p.update(pendict) + + _p_buf = {} + for key in p: + _p_buf[key] = _pd[key] + + if self.undobuffer: + self.undobuffer.push(("pen", _p_buf)) + + newLine = False + if "pendown" in p: + if self._drawing != p["pendown"]: + newLine = True + if "pencolor" in p: + if isinstance(p["pencolor"], tuple): + p["pencolor"] = self._colorstr((p["pencolor"],)) + if self._pencolor != p["pencolor"]: + newLine = True + if "pensize" in p: + if self._pensize != p["pensize"]: + newLine = True + if newLine: + self._newLine() + if "pendown" in p: + self._drawing = p["pendown"] + if "pencolor" in p: + self._pencolor = p["pencolor"] + if "pensize" in p: + self._pensize = p["pensize"] + if "fillcolor" in p: + if isinstance(p["fillcolor"], tuple): + p["fillcolor"] = self._colorstr((p["fillcolor"],)) + self._fillcolor = p["fillcolor"] + if "speed" in p: + self._speed = p["speed"] + if "resizemode" in p: + self._resizemode = p["resizemode"] + if "stretchfactor" in p: + sf = p["stretchfactor"] + if isinstance(sf, (int, float)): + sf = (sf, sf) + self._stretchfactor = sf + if "shearfactor" in p: + self._shearfactor = p["shearfactor"] + if "outline" in p: + self._outlinewidth = p["outline"] + if "shown" in p: + self._shown = p["shown"] + if "tilt" in p: + self._tilt = p["tilt"] + if "stretchfactor" in p or "tilt" in p or "shearfactor" in p: + scx, scy = self._stretchfactor + shf = self._shearfactor + sa, ca = math.sin(self._tilt), math.cos(self._tilt) + self._shapetrafo = ( scx*ca, scy*(shf*ca + sa), + -scx*sa, scy*(ca - shf*sa)) + self._update() + +## three dummy methods to be implemented by child class: + + def _newLine(self, usePos = True): + """dummy method - to be overwritten by child class""" + def _update(self, count=True, forced=False): + """dummy method - to be overwritten by child class""" + def _color(self, args): + """dummy method - to be overwritten by child class""" + def _colorstr(self, args): + """dummy method - to be overwritten by child class""" + + width = pensize + up = penup + pu = penup + pd = pendown + down = pendown + st = showturtle + ht = hideturtle + + +class _TurtleImage(object): + """Helper class: Datatype to store Turtle attributes + """ + + def __init__(self, screen, shapeIndex): + self.screen = screen + self._type = None + self._setshape(shapeIndex) + + def _setshape(self, shapeIndex): + screen = self.screen + self.shapeIndex = shapeIndex + if self._type == "polygon" == screen._shapes[shapeIndex]._type: + return + if self._type == "image" == screen._shapes[shapeIndex]._type: + return + if self._type in ["image", "polygon"]: + screen._delete(self._item) + elif self._type == "compound": + for item in self._item: + screen._delete(item) + self._type = screen._shapes[shapeIndex]._type + if self._type == "polygon": + self._item = screen._createpoly() + elif self._type == "image": + self._item = screen._createimage(screen._shapes["blank"]._data) + elif self._type == "compound": + self._item = [screen._createpoly() for item in + screen._shapes[shapeIndex]._data] + + +class RawTurtle(TPen, TNavigator): + """Animation part of the RawTurtle. + Puts RawTurtle upon a TurtleScreen and provides tools for + its animation. + """ + screens = [] + + def __init__(self, canvas=None, + shape=_CFG["shape"], + undobuffersize=_CFG["undobuffersize"], + visible=_CFG["visible"]): + if isinstance(canvas, _Screen): + self.screen = canvas + elif isinstance(canvas, TurtleScreen): + if canvas not in RawTurtle.screens: + RawTurtle.screens.append(canvas) + self.screen = canvas + elif isinstance(canvas, (ScrolledCanvas, Canvas)): + for screen in RawTurtle.screens: + if screen.cv == canvas: + self.screen = screen + break + else: + self.screen = TurtleScreen(canvas) + RawTurtle.screens.append(self.screen) + else: + raise TurtleGraphicsError("bad canvas argument %s" % canvas) + + screen = self.screen + TNavigator.__init__(self, screen.mode()) + TPen.__init__(self) + screen._turtles.append(self) + self.drawingLineItem = screen._createline() + self.turtle = _TurtleImage(screen, shape) + self._poly = None + self._creatingPoly = False + self._fillitem = self._fillpath = None + self._shown = visible + self._hidden_from_screen = False + self.currentLineItem = screen._createline() + self.currentLine = [self._position] + self.items = [self.currentLineItem] + self.stampItems = [] + self._undobuffersize = undobuffersize + self.undobuffer = Tbuffer(undobuffersize) + self._update() + + def reset(self): + """Delete the turtle's drawings and restore its default values. + + No argument. + + Delete the turtle's drawings from the screen, re-center the turtle + and set variables to the default values. + + Example (for a Turtle instance named turtle): + >>> turtle.position() + (0.00,-22.00) + >>> turtle.heading() + 100.0 + >>> turtle.reset() + >>> turtle.position() + (0.00,0.00) + >>> turtle.heading() + 0.0 + """ + TNavigator.reset(self) + TPen._reset(self) + self._clear() + self._drawturtle() + self._update() + + def setundobuffer(self, size): + """Set or disable undobuffer. + + Argument: + size -- an integer or None + + If size is an integer an empty undobuffer of given size is installed. + Size gives the maximum number of turtle-actions that can be undone + by the undo() function. + If size is None, no undobuffer is present. + + Example (for a Turtle instance named turtle): + >>> turtle.setundobuffer(42) + """ + if size is None or size <= 0: + self.undobuffer = None + else: + self.undobuffer = Tbuffer(size) + + def undobufferentries(self): + """Return count of entries in the undobuffer. + + No argument. + + Example (for a Turtle instance named turtle): + >>> while undobufferentries(): + ... undo() + """ + if self.undobuffer is None: + return 0 + return self.undobuffer.nr_of_items() + + def _clear(self): + """Delete all of pen's drawings""" + self._fillitem = self._fillpath = None + for item in self.items: + self.screen._delete(item) + self.currentLineItem = self.screen._createline() + self.currentLine = [] + if self._drawing: + self.currentLine.append(self._position) + self.items = [self.currentLineItem] + self.clearstamps() + self.setundobuffer(self._undobuffersize) + + + def clear(self): + """Delete the turtle's drawings from the screen. Do not move turtle. + + No arguments. + + Delete the turtle's drawings from the screen. Do not move turtle. + State and position of the turtle as well as drawings of other + turtles are not affected. + + Examples (for a Turtle instance named turtle): + >>> turtle.clear() + """ + self._clear() + self._update() + + def _update_data(self): + self.screen._incrementudc() + if self.screen._updatecounter != 0: + return + if len(self.currentLine)>1: + self.screen._drawline(self.currentLineItem, self.currentLine, + self._pencolor, self._pensize) + + def _update(self): + """Perform a Turtle-data update. + """ + screen = self.screen + if screen._tracing == 0: + return + elif screen._tracing == 1: + self._update_data() + self._drawturtle() + screen._update() # TurtleScreenBase + screen._delay(screen._delayvalue) # TurtleScreenBase + else: + self._update_data() + if screen._updatecounter == 0: + for t in screen.turtles(): + t._drawturtle() + screen._update() + + def _tracer(self, flag=None, delay=None): + """Turns turtle animation on/off and set delay for update drawings. + + Optional arguments: + n -- nonnegative integer + delay -- nonnegative integer + + If n is given, only each n-th regular screen update is really performed. + (Can be used to accelerate the drawing of complex graphics.) + Second arguments sets delay value (see RawTurtle.delay()) + + Example (for a Turtle instance named turtle): + >>> turtle.tracer(8, 25) + >>> dist = 2 + >>> for i in range(200): + ... turtle.fd(dist) + ... turtle.rt(90) + ... dist += 2 + """ + return self.screen.tracer(flag, delay) + + def _color(self, args): + return self.screen._color(args) + + def _colorstr(self, args): + return self.screen._colorstr(args) + + def _cc(self, args): + """Convert colortriples to hexstrings. + """ + if isinstance(args, str): + return args + try: + r, g, b = args + except: + raise TurtleGraphicsError("bad color arguments: %s" % str(args)) + if self.screen._colormode == 1.0: + r, g, b = [round(255.0*x) for x in (r, g, b)] + if not ((0 <= r <= 255) and (0 <= g <= 255) and (0 <= b <= 255)): + raise TurtleGraphicsError("bad color sequence: %s" % str(args)) + return "#%02x%02x%02x" % (r, g, b) + + def clone(self): + """Create and return a clone of the turtle. + + No argument. + + Create and return a clone of the turtle with same position, heading + and turtle properties. + + Example (for a Turtle instance named mick): + mick = Turtle() + joe = mick.clone() + """ + screen = self.screen + self._newLine(self._drawing) + + turtle = self.turtle + self.screen = None + self.turtle = None # too make self deepcopy-able + + q = deepcopy(self) + + self.screen = screen + self.turtle = turtle + + q.screen = screen + q.turtle = _TurtleImage(screen, self.turtle.shapeIndex) + + screen._turtles.append(q) + ttype = screen._shapes[self.turtle.shapeIndex]._type + if ttype == "polygon": + q.turtle._item = screen._createpoly() + elif ttype == "image": + q.turtle._item = screen._createimage(screen._shapes["blank"]._data) + elif ttype == "compound": + q.turtle._item = [screen._createpoly() for item in + screen._shapes[self.turtle.shapeIndex]._data] + q.currentLineItem = screen._createline() + q._update() + return q + + def shape(self, name=None): + """Set turtle shape to shape with given name / return current shapename. + + Optional argument: + name -- a string, which is a valid shapename + + Set turtle shape to shape with given name or, if name is not given, + return name of current shape. + Shape with name must exist in the TurtleScreen's shape dictionary. + Initially there are the following polygon shapes: + 'arrow', 'turtle', 'circle', 'square', 'triangle', 'classic'. + To learn about how to deal with shapes see Screen-method register_shape. + + Example (for a Turtle instance named turtle): + >>> turtle.shape() + 'arrow' + >>> turtle.shape("turtle") + >>> turtle.shape() + 'turtle' + """ + if name is None: + return self.turtle.shapeIndex + if not name in self.screen.getshapes(): + raise TurtleGraphicsError("There is no shape named %s" % name) + self.turtle._setshape(name) + self._update() + + def shapesize(self, stretch_wid=None, stretch_len=None, outline=None): + """Set/return turtle's stretchfactors/outline. Set resizemode to "user". + + Optional arguments: + stretch_wid : positive number + stretch_len : positive number + outline : positive number + + Return or set the pen's attributes x/y-stretchfactors and/or outline. + Set resizemode to "user". + If and only if resizemode is set to "user", the turtle will be displayed + stretched according to its stretchfactors: + stretch_wid is stretchfactor perpendicular to orientation + stretch_len is stretchfactor in direction of turtles orientation. + outline determines the width of the shapes's outline. + + Examples (for a Turtle instance named turtle): + >>> turtle.resizemode("user") + >>> turtle.shapesize(5, 5, 12) + >>> turtle.shapesize(outline=8) + """ + if stretch_wid is stretch_len is outline is None: + stretch_wid, stretch_len = self._stretchfactor + return stretch_wid, stretch_len, self._outlinewidth + if stretch_wid == 0 or stretch_len == 0: + raise TurtleGraphicsError("stretch_wid/stretch_len must not be zero") + if stretch_wid is not None: + if stretch_len is None: + stretchfactor = stretch_wid, stretch_wid + else: + stretchfactor = stretch_wid, stretch_len + elif stretch_len is not None: + stretchfactor = self._stretchfactor[0], stretch_len + else: + stretchfactor = self._stretchfactor + if outline is None: + outline = self._outlinewidth + self.pen(resizemode="user", + stretchfactor=stretchfactor, outline=outline) + + def shearfactor(self, shear=None): + """Set or return the current shearfactor. + + Optional argument: shear -- number, tangent of the shear angle + + Shear the turtleshape according to the given shearfactor shear, + which is the tangent of the shear angle. DO NOT change the + turtle's heading (direction of movement). + If shear is not given: return the current shearfactor, i. e. the + tangent of the shear angle, by which lines parallel to the + heading of the turtle are sheared. + + Examples (for a Turtle instance named turtle): + >>> turtle.shape("circle") + >>> turtle.shapesize(5,2) + >>> turtle.shearfactor(0.5) + >>> turtle.shearfactor() + >>> 0.5 + """ + if shear is None: + return self._shearfactor + self.pen(resizemode="user", shearfactor=shear) + + def settiltangle(self, angle): + """Rotate the turtleshape to point in the specified direction + + Argument: angle -- number + + Rotate the turtleshape to point in the direction specified by angle, + regardless of its current tilt-angle. DO NOT change the turtle's + heading (direction of movement). + + + Examples (for a Turtle instance named turtle): + >>> turtle.shape("circle") + >>> turtle.shapesize(5,2) + >>> turtle.settiltangle(45) + >>> stamp() + >>> turtle.fd(50) + >>> turtle.settiltangle(-45) + >>> stamp() + >>> turtle.fd(50) + """ + tilt = -angle * self._degreesPerAU * self._angleOrient + tilt = (tilt * math.pi / 180.0) % (2*math.pi) + self.pen(resizemode="user", tilt=tilt) + + def tiltangle(self, angle=None): + """Set or return the current tilt-angle. + + Optional argument: angle -- number + + Rotate the turtleshape to point in the direction specified by angle, + regardless of its current tilt-angle. DO NOT change the turtle's + heading (direction of movement). + If angle is not given: return the current tilt-angle, i. e. the angle + between the orientation of the turtleshape and the heading of the + turtle (its direction of movement). + + Deprecated since Python 3.1 + + Examples (for a Turtle instance named turtle): + >>> turtle.shape("circle") + >>> turtle.shapesize(5,2) + >>> turtle.tilt(45) + >>> turtle.tiltangle() + """ + if angle is None: + tilt = -self._tilt * (180.0/math.pi) * self._angleOrient + return (tilt / self._degreesPerAU) % self._fullcircle + else: + self.settiltangle(angle) + + def tilt(self, angle): + """Rotate the turtleshape by angle. + + Argument: + angle - a number + + Rotate the turtleshape by angle from its current tilt-angle, + but do NOT change the turtle's heading (direction of movement). + + Examples (for a Turtle instance named turtle): + >>> turtle.shape("circle") + >>> turtle.shapesize(5,2) + >>> turtle.tilt(30) + >>> turtle.fd(50) + >>> turtle.tilt(30) + >>> turtle.fd(50) + """ + self.settiltangle(angle + self.tiltangle()) + + def shapetransform(self, t11=None, t12=None, t21=None, t22=None): + """Set or return the current transformation matrix of the turtle shape. + + Optional arguments: t11, t12, t21, t22 -- numbers. + + If none of the matrix elements are given, return the transformation + matrix. + Otherwise set the given elements and transform the turtleshape + according to the matrix consisting of first row t11, t12 and + second row t21, 22. + Modify stretchfactor, shearfactor and tiltangle according to the + given matrix. + + Examples (for a Turtle instance named turtle): + >>> turtle.shape("square") + >>> turtle.shapesize(4,2) + >>> turtle.shearfactor(-0.5) + >>> turtle.shapetransform() + (4.0, -1.0, -0.0, 2.0) + """ + if t11 is t12 is t21 is t22 is None: + return self._shapetrafo + m11, m12, m21, m22 = self._shapetrafo + if t11 is not None: m11 = t11 + if t12 is not None: m12 = t12 + if t21 is not None: m21 = t21 + if t22 is not None: m22 = t22 + if t11 * t22 - t12 * t21 == 0: + raise TurtleGraphicsError("Bad shape transform matrix: must not be singular") + self._shapetrafo = (m11, m12, m21, m22) + alfa = math.atan2(-m21, m11) % (2 * math.pi) + sa, ca = math.sin(alfa), math.cos(alfa) + a11, a12, a21, a22 = (ca*m11 - sa*m21, ca*m12 - sa*m22, + sa*m11 + ca*m21, sa*m12 + ca*m22) + self._stretchfactor = a11, a22 + self._shearfactor = a12/a22 + self._tilt = alfa + self.pen(resizemode="user") + + + def _polytrafo(self, poly): + """Computes transformed polygon shapes from a shape + according to current position and heading. + """ + screen = self.screen + p0, p1 = self._position + e0, e1 = self._orient + e = Vec2D(e0, e1 * screen.yscale / screen.xscale) + e0, e1 = (1.0 / abs(e)) * e + return [(p0+(e1*x+e0*y)/screen.xscale, p1+(-e0*x+e1*y)/screen.yscale) + for (x, y) in poly] + + def get_shapepoly(self): + """Return the current shape polygon as tuple of coordinate pairs. + + No argument. + + Examples (for a Turtle instance named turtle): + >>> turtle.shape("square") + >>> turtle.shapetransform(4, -1, 0, 2) + >>> turtle.get_shapepoly() + ((50, -20), (30, 20), (-50, 20), (-30, -20)) + + """ + shape = self.screen._shapes[self.turtle.shapeIndex] + if shape._type == "polygon": + return self._getshapepoly(shape._data, shape._type == "compound") + # else return None + + def _getshapepoly(self, polygon, compound=False): + """Calculate transformed shape polygon according to resizemode + and shapetransform. + """ + if self._resizemode == "user" or compound: + t11, t12, t21, t22 = self._shapetrafo + elif self._resizemode == "auto": + l = max(1, self._pensize/5.0) + t11, t12, t21, t22 = l, 0, 0, l + elif self._resizemode == "noresize": + return polygon + return tuple([(t11*x + t12*y, t21*x + t22*y) for (x, y) in polygon]) + + def _drawturtle(self): + """Manages the correct rendering of the turtle with respect to + its shape, resizemode, stretch and tilt etc.""" + screen = self.screen + shape = screen._shapes[self.turtle.shapeIndex] + ttype = shape._type + titem = self.turtle._item + if self._shown and screen._updatecounter == 0 and screen._tracing > 0: + self._hidden_from_screen = False + tshape = shape._data + if ttype == "polygon": + if self._resizemode == "noresize": w = 1 + elif self._resizemode == "auto": w = self._pensize + else: w =self._outlinewidth + shape = self._polytrafo(self._getshapepoly(tshape)) + fc, oc = self._fillcolor, self._pencolor + screen._drawpoly(titem, shape, fill=fc, outline=oc, + width=w, top=True) + elif ttype == "image": + screen._drawimage(titem, self._position, tshape) + elif ttype == "compound": + for item, (poly, fc, oc) in zip(titem, tshape): + poly = self._polytrafo(self._getshapepoly(poly, True)) + screen._drawpoly(item, poly, fill=self._cc(fc), + outline=self._cc(oc), width=self._outlinewidth, top=True) + else: + if self._hidden_from_screen: + return + if ttype == "polygon": + screen._drawpoly(titem, ((0, 0), (0, 0), (0, 0)), "", "") + elif ttype == "image": + screen._drawimage(titem, self._position, + screen._shapes["blank"]._data) + elif ttype == "compound": + for item in titem: + screen._drawpoly(item, ((0, 0), (0, 0), (0, 0)), "", "") + self._hidden_from_screen = True + +############################## stamp stuff ############################### + + def stamp(self): + """Stamp a copy of the turtleshape onto the canvas and return its id. + + No argument. + + Stamp a copy of the turtle shape onto the canvas at the current + turtle position. Return a stamp_id for that stamp, which can be + used to delete it by calling clearstamp(stamp_id). + + Example (for a Turtle instance named turtle): + >>> turtle.color("blue") + >>> turtle.stamp() + 13 + >>> turtle.fd(50) + """ + screen = self.screen + shape = screen._shapes[self.turtle.shapeIndex] + ttype = shape._type + tshape = shape._data + if ttype == "polygon": + stitem = screen._createpoly() + if self._resizemode == "noresize": w = 1 + elif self._resizemode == "auto": w = self._pensize + else: w =self._outlinewidth + shape = self._polytrafo(self._getshapepoly(tshape)) + fc, oc = self._fillcolor, self._pencolor + screen._drawpoly(stitem, shape, fill=fc, outline=oc, + width=w, top=True) + elif ttype == "image": + stitem = screen._createimage("") + screen._drawimage(stitem, self._position, tshape) + elif ttype == "compound": + stitem = [] + for element in tshape: + item = screen._createpoly() + stitem.append(item) + stitem = tuple(stitem) + for item, (poly, fc, oc) in zip(stitem, tshape): + poly = self._polytrafo(self._getshapepoly(poly, True)) + screen._drawpoly(item, poly, fill=self._cc(fc), + outline=self._cc(oc), width=self._outlinewidth, top=True) + self.stampItems.append(stitem) + self.undobuffer.push(("stamp", stitem)) + return stitem + + def _clearstamp(self, stampid): + """does the work for clearstamp() and clearstamps() + """ + if stampid in self.stampItems: + if isinstance(stampid, tuple): + for subitem in stampid: + self.screen._delete(subitem) + else: + self.screen._delete(stampid) + self.stampItems.remove(stampid) + # Delete stampitem from undobuffer if necessary + # if clearstamp is called directly. + item = ("stamp", stampid) + buf = self.undobuffer + if item not in buf.buffer: + return + index = buf.buffer.index(item) + buf.buffer.remove(item) + if index <= buf.ptr: + buf.ptr = (buf.ptr - 1) % buf.bufsize + buf.buffer.insert((buf.ptr+1)%buf.bufsize, [None]) + + def clearstamp(self, stampid): + """Delete stamp with given stampid + + Argument: + stampid - an integer, must be return value of previous stamp() call. + + Example (for a Turtle instance named turtle): + >>> turtle.color("blue") + >>> astamp = turtle.stamp() + >>> turtle.fd(50) + >>> turtle.clearstamp(astamp) + """ + self._clearstamp(stampid) + self._update() + + def clearstamps(self, n=None): + """Delete all or first/last n of turtle's stamps. + + Optional argument: + n -- an integer + + If n is None, delete all of pen's stamps, + else if n > 0 delete first n stamps + else if n < 0 delete last n stamps. + + Example (for a Turtle instance named turtle): + >>> for i in range(8): + ... turtle.stamp(); turtle.fd(30) + ... + >>> turtle.clearstamps(2) + >>> turtle.clearstamps(-2) + >>> turtle.clearstamps() + """ + if n is None: + toDelete = self.stampItems[:] + elif n >= 0: + toDelete = self.stampItems[:n] + else: + toDelete = self.stampItems[n:] + for item in toDelete: + self._clearstamp(item) + self._update() + + def _goto(self, end): + """Move the pen to the point end, thereby drawing a line + if pen is down. All other methods for turtle movement depend + on this one. + """ + ## Version with undo-stuff + go_modes = ( self._drawing, + self._pencolor, + self._pensize, + isinstance(self._fillpath, list)) + screen = self.screen + undo_entry = ("go", self._position, end, go_modes, + (self.currentLineItem, + self.currentLine[:], + screen._pointlist(self.currentLineItem), + self.items[:]) + ) + if self.undobuffer: + self.undobuffer.push(undo_entry) + start = self._position + if self._speed and screen._tracing == 1: + diff = (end-start) + diffsq = (diff[0]*screen.xscale)**2 + (diff[1]*screen.yscale)**2 + nhops = 1+int((diffsq**0.5)/(3*(1.1**self._speed)*self._speed)) + delta = diff * (1.0/nhops) + for n in range(1, nhops): + if n == 1: + top = True + else: + top = False + self._position = start + delta * n + if self._drawing: + screen._drawline(self.drawingLineItem, + (start, self._position), + self._pencolor, self._pensize, top) + self._update() + if self._drawing: + screen._drawline(self.drawingLineItem, ((0, 0), (0, 0)), + fill="", width=self._pensize) + # Turtle now at end, + if self._drawing: # now update currentLine + self.currentLine.append(end) + if isinstance(self._fillpath, list): + self._fillpath.append(end) + ###### vererbung!!!!!!!!!!!!!!!!!!!!!! + self._position = end + if self._creatingPoly: + self._poly.append(end) + if len(self.currentLine) > 42: # 42! answer to the ultimate question + # of life, the universe and everything + self._newLine() + self._update() #count=True) + + def _undogoto(self, entry): + """Reverse a _goto. Used for undo() + """ + old, new, go_modes, coodata = entry + drawing, pc, ps, filling = go_modes + cLI, cL, pl, items = coodata + screen = self.screen + if abs(self._position - new) > 0.5: + print ("undogoto: HALLO-DA-STIMMT-WAS-NICHT!") + # restore former situation + self.currentLineItem = cLI + self.currentLine = cL + + if pl == [(0, 0), (0, 0)]: + usepc = "" + else: + usepc = pc + screen._drawline(cLI, pl, fill=usepc, width=ps) + + todelete = [i for i in self.items if (i not in items) and + (screen._type(i) == "line")] + for i in todelete: + screen._delete(i) + self.items.remove(i) + + start = old + if self._speed and screen._tracing == 1: + diff = old - new + diffsq = (diff[0]*screen.xscale)**2 + (diff[1]*screen.yscale)**2 + nhops = 1+int((diffsq**0.5)/(3*(1.1**self._speed)*self._speed)) + delta = diff * (1.0/nhops) + for n in range(1, nhops): + if n == 1: + top = True + else: + top = False + self._position = new + delta * n + if drawing: + screen._drawline(self.drawingLineItem, + (start, self._position), + pc, ps, top) + self._update() + if drawing: + screen._drawline(self.drawingLineItem, ((0, 0), (0, 0)), + fill="", width=ps) + # Turtle now at position old, + self._position = old + ## if undo is done during creating a polygon, the last vertex + ## will be deleted. if the polygon is entirely deleted, + ## creatingPoly will be set to False. + ## Polygons created before the last one will not be affected by undo() + if self._creatingPoly: + if len(self._poly) > 0: + self._poly.pop() + if self._poly == []: + self._creatingPoly = False + self._poly = None + if filling: + if self._fillpath == []: + self._fillpath = None + print("Unwahrscheinlich in _undogoto!") + elif self._fillpath is not None: + self._fillpath.pop() + self._update() #count=True) + + def _rotate(self, angle): + """Turns pen clockwise by angle. + """ + if self.undobuffer: + self.undobuffer.push(("rot", angle, self._degreesPerAU)) + angle *= self._degreesPerAU + neworient = self._orient.rotate(angle) + tracing = self.screen._tracing + if tracing == 1 and self._speed > 0: + anglevel = 3.0 * self._speed + steps = 1 + int(abs(angle)/anglevel) + delta = 1.0*angle/steps + for _ in range(steps): + self._orient = self._orient.rotate(delta) + self._update() + self._orient = neworient + self._update() + + def _newLine(self, usePos=True): + """Closes current line item and starts a new one. + Remark: if current line became too long, animation + performance (via _drawline) slowed down considerably. + """ + if len(self.currentLine) > 1: + self.screen._drawline(self.currentLineItem, self.currentLine, + self._pencolor, self._pensize) + self.currentLineItem = self.screen._createline() + self.items.append(self.currentLineItem) + else: + self.screen._drawline(self.currentLineItem, top=True) + self.currentLine = [] + if usePos: + self.currentLine = [self._position] + + def filling(self): + """Return fillstate (True if filling, False else). + + No argument. + + Example (for a Turtle instance named turtle): + >>> turtle.begin_fill() + >>> if turtle.filling(): + ... turtle.pensize(5) + ... else: + ... turtle.pensize(3) + """ + return isinstance(self._fillpath, list) + + def begin_fill(self): + """Called just before drawing a shape to be filled. + + No argument. + + Example (for a Turtle instance named turtle): + >>> turtle.color("black", "red") + >>> turtle.begin_fill() + >>> turtle.circle(60) + >>> turtle.end_fill() + """ + if not self.filling(): + self._fillitem = self.screen._createpoly() + self.items.append(self._fillitem) + self._fillpath = [self._position] + self._newLine() + if self.undobuffer: + self.undobuffer.push(("beginfill", self._fillitem)) + self._update() + + + def end_fill(self): + """Fill the shape drawn after the call begin_fill(). + + No argument. + + Example (for a Turtle instance named turtle): + >>> turtle.color("black", "red") + >>> turtle.begin_fill() + >>> turtle.circle(60) + >>> turtle.end_fill() + """ + if self.filling(): + if len(self._fillpath) > 2: + self.screen._drawpoly(self._fillitem, self._fillpath, + fill=self._fillcolor) + if self.undobuffer: + self.undobuffer.push(("dofill", self._fillitem)) + self._fillitem = self._fillpath = None + self._update() + + def dot(self, size=None, *color): + """Draw a dot with diameter size, using color. + + Optional arguments: + size -- an integer >= 1 (if given) + color -- a colorstring or a numeric color tuple + + Draw a circular dot with diameter size, using color. + If size is not given, the maximum of pensize+4 and 2*pensize is used. + + Example (for a Turtle instance named turtle): + >>> turtle.dot() + >>> turtle.fd(50); turtle.dot(20, "blue"); turtle.fd(50) + """ + if not color: + if isinstance(size, (str, tuple)): + color = self._colorstr(size) + size = self._pensize + max(self._pensize, 4) + else: + color = self._pencolor + if not size: + size = self._pensize + max(self._pensize, 4) + else: + if size is None: + size = self._pensize + max(self._pensize, 4) + color = self._colorstr(color) + if hasattr(self.screen, "_dot"): + item = self.screen._dot(self._position, size, color) + self.items.append(item) + if self.undobuffer: + self.undobuffer.push(("dot", item)) + else: + pen = self.pen() + if self.undobuffer: + self.undobuffer.push(["seq"]) + self.undobuffer.cumulate = True + try: + if self.resizemode() == 'auto': + self.ht() + self.pendown() + self.pensize(size) + self.pencolor(color) + self.forward(0) + finally: + self.pen(pen) + if self.undobuffer: + self.undobuffer.cumulate = False + + def _write(self, txt, align, font): + """Performs the writing for write() + """ + item, end = self.screen._write(self._position, txt, align, font, + self._pencolor) + self.items.append(item) + if self.undobuffer: + self.undobuffer.push(("wri", item)) + return end + + def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")): + """Write text at the current turtle position. + + Arguments: + arg -- info, which is to be written to the TurtleScreen + move (optional) -- True/False + align (optional) -- one of the strings "left", "center" or right" + font (optional) -- a triple (fontname, fontsize, fonttype) + + Write text - the string representation of arg - at the current + turtle position according to align ("left", "center" or right") + and with the given font. + If move is True, the pen is moved to the bottom-right corner + of the text. By default, move is False. + + Example (for a Turtle instance named turtle): + >>> turtle.write('Home = ', True, align="center") + >>> turtle.write((0,0), True) + """ + if self.undobuffer: + self.undobuffer.push(["seq"]) + self.undobuffer.cumulate = True + end = self._write(str(arg), align.lower(), font) + if move: + x, y = self.pos() + self.setpos(end, y) + if self.undobuffer: + self.undobuffer.cumulate = False + + def begin_poly(self): + """Start recording the vertices of a polygon. + + No argument. + + Start recording the vertices of a polygon. Current turtle position + is first point of polygon. + + Example (for a Turtle instance named turtle): + >>> turtle.begin_poly() + """ + self._poly = [self._position] + self._creatingPoly = True + + def end_poly(self): + """Stop recording the vertices of a polygon. + + No argument. + + Stop recording the vertices of a polygon. Current turtle position is + last point of polygon. This will be connected with the first point. + + Example (for a Turtle instance named turtle): + >>> turtle.end_poly() + """ + self._creatingPoly = False + + def get_poly(self): + """Return the lastly recorded polygon. + + No argument. + + Example (for a Turtle instance named turtle): + >>> p = turtle.get_poly() + >>> turtle.register_shape("myFavouriteShape", p) + """ + ## check if there is any poly? + if self._poly is not None: + return tuple(self._poly) + + def getscreen(self): + """Return the TurtleScreen object, the turtle is drawing on. + + No argument. + + Return the TurtleScreen object, the turtle is drawing on. + So TurtleScreen-methods can be called for that object. + + Example (for a Turtle instance named turtle): + >>> ts = turtle.getscreen() + >>> ts + + >>> ts.bgcolor("pink") + """ + return self.screen + + def getturtle(self): + """Return the Turtleobject itself. + + No argument. + + Only reasonable use: as a function to return the 'anonymous turtle': + + Example: + >>> pet = getturtle() + >>> pet.fd(50) + >>> pet + + >>> turtles() + [] + """ + return self + + getpen = getturtle + + + ################################################################ + ### screen oriented methods recurring to methods of TurtleScreen + ################################################################ + + def _delay(self, delay=None): + """Set delay value which determines speed of turtle animation. + """ + return self.screen.delay(delay) + + def onclick(self, fun, btn=1, add=None): + """Bind fun to mouse-click event on this turtle on canvas. + + Arguments: + fun -- a function with two arguments, to which will be assigned + the coordinates of the clicked point on the canvas. + num -- number of the mouse-button defaults to 1 (left mouse button). + add -- True or False. If True, new binding will be added, otherwise + it will replace a former binding. + + Example for the anonymous turtle, i. e. the procedural way: + + >>> def turn(x, y): + ... left(360) + ... + >>> onclick(turn) # Now clicking into the turtle will turn it. + >>> onclick(None) # event-binding will be removed + """ + self.screen._onclick(self.turtle._item, fun, btn, add) + self._update() + + def onrelease(self, fun, btn=1, add=None): + """Bind fun to mouse-button-release event on this turtle on canvas. + + Arguments: + fun -- a function with two arguments, to which will be assigned + the coordinates of the clicked point on the canvas. + num -- number of the mouse-button defaults to 1 (left mouse button). + + Example (for a MyTurtle instance named joe): + >>> class MyTurtle(Turtle): + ... def glow(self,x,y): + ... self.fillcolor("red") + ... def unglow(self,x,y): + ... self.fillcolor("") + ... + >>> joe = MyTurtle() + >>> joe.onclick(joe.glow) + >>> joe.onrelease(joe.unglow) + + Clicking on joe turns fillcolor red, unclicking turns it to + transparent. + """ + self.screen._onrelease(self.turtle._item, fun, btn, add) + self._update() + + def ondrag(self, fun, btn=1, add=None): + """Bind fun to mouse-move event on this turtle on canvas. + + Arguments: + fun -- a function with two arguments, to which will be assigned + the coordinates of the clicked point on the canvas. + num -- number of the mouse-button defaults to 1 (left mouse button). + + Every sequence of mouse-move-events on a turtle is preceded by a + mouse-click event on that turtle. + + Example (for a Turtle instance named turtle): + >>> turtle.ondrag(turtle.goto) + + Subsequently clicking and dragging a Turtle will move it + across the screen thereby producing handdrawings (if pen is + down). + """ + self.screen._ondrag(self.turtle._item, fun, btn, add) + + + def _undo(self, action, data): + """Does the main part of the work for undo() + """ + if self.undobuffer is None: + return + if action == "rot": + angle, degPAU = data + self._rotate(-angle*degPAU/self._degreesPerAU) + dummy = self.undobuffer.pop() + elif action == "stamp": + stitem = data[0] + self.clearstamp(stitem) + elif action == "go": + self._undogoto(data) + elif action in ["wri", "dot"]: + item = data[0] + self.screen._delete(item) + self.items.remove(item) + elif action == "dofill": + item = data[0] + self.screen._drawpoly(item, ((0, 0),(0, 0),(0, 0)), + fill="", outline="") + elif action == "beginfill": + item = data[0] + self._fillitem = self._fillpath = None + if item in self.items: + self.screen._delete(item) + self.items.remove(item) + elif action == "pen": + TPen.pen(self, data[0]) + self.undobuffer.pop() + + def undo(self): + """undo (repeatedly) the last turtle action. + + No argument. + + undo (repeatedly) the last turtle action. + Number of available undo actions is determined by the size of + the undobuffer. + + Example (for a Turtle instance named turtle): + >>> for i in range(4): + ... turtle.fd(50); turtle.lt(80) + ... + >>> for i in range(8): + ... turtle.undo() + ... + """ + if self.undobuffer is None: + return + item = self.undobuffer.pop() + action = item[0] + data = item[1:] + if action == "seq": + while data: + item = data.pop() + self._undo(item[0], item[1:]) + else: + self._undo(action, data) + + turtlesize = shapesize + +RawPen = RawTurtle + +### Screen - Singleton ######################## + +def Screen(): + """Return the singleton screen object. + If none exists at the moment, create a new one and return it, + else return the existing one.""" + if Turtle._screen is None: + from __main__ import shell + Turtle._screen = TurtleScreen(WebCanvas(shell)) + return Turtle._screen + +class _Screen(TurtleScreen): + + _root = None + _canvas = None + _title = _CFG["title"] + + def __init__(self): + # XXX there is no need for this code to be conditional, + # as there will be only a single _Screen instance, anyway + # XXX actually, the turtle demo is injecting root window, + # so perhaps the conditional creation of a root should be + # preserved (perhaps by passing it as an optional parameter) + if _Screen._root is None: + _Screen._root = self._root = _Root() + self._root.title(_Screen._title) + self._root.ondestroy(self._destroy) + if _Screen._canvas is None: + width = _CFG["width"] + height = _CFG["height"] + canvwidth = _CFG["canvwidth"] + canvheight = _CFG["canvheight"] + leftright = _CFG["leftright"] + topbottom = _CFG["topbottom"] + self._root.setupcanvas(width, height, canvwidth, canvheight) + _Screen._canvas = self._root._getcanvas() + TurtleScreen.__init__(self, _Screen._canvas) + self.setup(width, height, leftright, topbottom) + + def setup(self, width=_CFG["width"], height=_CFG["height"], + startx=_CFG["leftright"], starty=_CFG["topbottom"]): + """ Set the size and position of the main window. + + Arguments: + width: as integer a size in pixels, as float a fraction of the screen. + Default is 50% of screen. + height: as integer the height in pixels, as float a fraction of the + screen. Default is 75% of screen. + startx: if positive, starting position in pixels from the left + edge of the screen, if negative from the right edge + Default, startx=None is to center window horizontally. + starty: if positive, starting position in pixels from the top + edge of the screen, if negative from the bottom edge + Default, starty=None is to center window vertically. + + Examples (for a Screen instance named screen): + >>> screen.setup (width=200, height=200, startx=0, starty=0) + + sets window to 200x200 pixels, in upper left of screen + + >>> screen.setup(width=.75, height=0.5, startx=None, starty=None) + + sets window to 75% of screen by 50% of screen and centers + """ + if not hasattr(self._root, "set_geometry"): + return + sw = self._root.win_width() + sh = self._root.win_height() + if isinstance(width, float) and 0 <= width <= 1: + width = sw*width + if startx is None: + startx = (sw - width) / 2 + if isinstance(height, float) and 0 <= height <= 1: + height = sh*height + if starty is None: + starty = (sh - height) / 2 + self._root.set_geometry(width, height, startx, starty) + self.update() + + def title(self, titlestring): + """Set title of turtle-window + + Argument: + titlestring -- a string, to appear in the titlebar of the + turtle graphics window. + + This is a method of Screen-class. Not available for TurtleScreen- + objects. + + Example (for a Screen instance named screen): + >>> screen.title("Welcome to the turtle-zoo!") + """ + if _Screen._root is not None: + _Screen._root.title(titlestring) + _Screen._title = titlestring + + def _destroy(self): + root = self._root + if root is _Screen._root: + Turtle._pen = None + Turtle._screen = None + _Screen._root = None + _Screen._canvas = None + TurtleScreen._RUNNING = True + root.destroy() + + def bye(self): + """Shut the turtlegraphics window. + + Example (for a TurtleScreen instance named screen): + >>> screen.bye() + """ + self._destroy() + + def exitonclick(self): + """Go into mainloop until the mouse is clicked. + + No arguments. + + Bind bye() method to mouseclick on TurtleScreen. + If "using_IDLE" - value in configuration dictionary is False + (default value), enter mainloop. + If IDLE with -n switch (no subprocess) is used, this value should be + set to True in turtle.cfg. In this case IDLE's mainloop + is active also for the client script. + + This is a method of the Screen-class and not available for + TurtleScreen instances. + + Example (for a Screen instance named screen): + >>> screen.exitonclick() + + """ + def exitGracefully(x, y): + """Screen.bye() with two dummy-parameters""" + self.bye() + self.onclick(exitGracefully) + if _CFG["using_IDLE"]: + return + try: + mainloop() + except AttributeError: + exit(0) + + +class Turtle(RawTurtle): + """RawTurtle auto-creating (scrolled) canvas. + + When a Turtle object is created or a function derived from some + Turtle method is called a TurtleScreen object is automatically created. + """ + _pen = None + _screen = None + + def __init__(self, + shape=_CFG["shape"], + undobuffersize=_CFG["undobuffersize"], + visible=_CFG["visible"]): + if Turtle._screen is None: + Turtle._screen = Screen() + RawTurtle.__init__(self, Turtle._screen, + shape=shape, + undobuffersize=undobuffersize, + visible=visible) + +Pen = Turtle + +def _getpen(): + """Create the 'anonymous' turtle if not already present.""" + if Turtle._pen is None: + Turtle._pen = Turtle() + return Turtle._pen + +def _getscreen(): + """Create a TurtleScreen if not already present.""" + if Turtle._screen is None: + Turtle._screen = Screen() + return Turtle._screen + +def write_docstringdict(filename="turtle_docstringdict"): + """Create and write docstring-dictionary to file. + + Optional argument: + filename -- a string, used as filename + default value is turtle_docstringdict + + Has to be called explicitly, (not used by the turtle-graphics classes) + The docstring dictionary will be written to the Python script .py + It is intended to serve as a template for translation of the docstrings + into different languages. + """ + docsdict = {} + + for methodname in _tg_screen_functions: + key = "_Screen."+methodname + docsdict[key] = eval(key).__doc__ + for methodname in _tg_turtle_functions: + key = "Turtle."+methodname + docsdict[key] = eval(key).__doc__ + + with open("%s.py" % filename,"w") as f: + keys = sorted([x for x in docsdict.keys() + if x.split('.')[1] not in _alias_list]) + f.write('docsdict = {\n\n') + for key in keys[:-1]: + f.write('%s :\n' % repr(key)) + f.write(' """%s\n""",\n\n' % docsdict[key]) + key = keys[-1] + f.write('%s :\n' % repr(key)) + f.write(' """%s\n"""\n\n' % docsdict[key]) + f.write("}\n") + f.close() + +def read_docstrings(lang): + """Read in docstrings from lang-specific docstring dictionary. + + Transfer docstrings, translated to lang, from a dictionary-file + to the methods of classes Screen and Turtle and - in revised form - + to the corresponding functions. + """ + modname = "turtle_docstringdict_%(language)s" % {'language':lang.lower()} + module = __import__(modname) + docsdict = module.docsdict + for key in docsdict: + try: +# eval(key).im_func.__doc__ = docsdict[key] + eval(key).__doc__ = docsdict[key] + except: + print("Bad docstring-entry: %s" % key) + +_LANGUAGE = _CFG["language"] + +try: + if _LANGUAGE != "english": + read_docstrings(_LANGUAGE) +except ImportError: + print("Cannot find docsdict for", _LANGUAGE) +except: + print ("Unknown Error when trying to import %s-docstring-dictionary" % + _LANGUAGE) + + +def getmethparlist(ob): + """Get strings describing the arguments for the given object + + Returns a pair of strings representing function parameter lists + including parenthesis. The first string is suitable for use in + function definition and the second is suitable for use in function + call. The "self" parameter is not included. + """ + defText = callText = "" + # bit of a hack for methods - turn it into a function + # but we drop the "self" param. + # Try and build one for Python defined functions + args, varargs, varkw = inspect.getargs(ob.__code__) + items2 = args[1:] + realArgs = args[1:] + defaults = ob.__defaults__ or [] + defaults = ["=%r" % (value,) for value in defaults] + defaults = [""] * (len(realArgs)-len(defaults)) + defaults + items1 = [arg + dflt for arg, dflt in zip(realArgs, defaults)] + if varargs is not None: + items1.append("*" + varargs) + items2.append("*" + varargs) + if varkw is not None: + items1.append("**" + varkw) + items2.append("**" + varkw) + defText = ", ".join(items1) + defText = "(%s)" % defText + callText = ", ".join(items2) + callText = "(%s)" % callText + return defText, callText + +def _turtle_docrevise(docstr): + """To reduce docstrings from RawTurtle class for functions + """ + import re + if docstr is None: + return None + turtlename = _CFG["exampleturtle"] + newdocstr = docstr.replace("%s." % turtlename,"") + parexp = re.compile(r' \(.+ %s\):' % turtlename) + newdocstr = parexp.sub(":", newdocstr) + return newdocstr + +def _screen_docrevise(docstr): + """To reduce docstrings from TurtleScreen class for functions + """ + import re + if docstr is None: + return None + screenname = _CFG["examplescreen"] + newdocstr = docstr.replace("%s." % screenname,"") + parexp = re.compile(r' \(.+ %s\):' % screenname) + newdocstr = parexp.sub(":", newdocstr) + return newdocstr + +## The following mechanism makes all methods of RawTurtle and Turtle available +## as functions. So we can enhance, change, add, delete methods to these +## classes and do not need to change anything here. + + +for methodname in _tg_screen_functions: + pl1, pl2 = getmethparlist(eval('_Screen.' + methodname)) + if pl1 == "": + print(">>>>>>", pl1, pl2) + continue + defstr = ("def %(key)s%(pl1)s: return _getscreen().%(key)s%(pl2)s" % + {'key':methodname, 'pl1':pl1, 'pl2':pl2}) + exec(defstr) + eval(methodname).__doc__ = _screen_docrevise(eval('_Screen.'+methodname).__doc__) + +for methodname in _tg_turtle_functions: + pl1, pl2 = getmethparlist(eval('Turtle.' + methodname)) + if pl1 == "": + print(">>>>>>", pl1, pl2) + continue + defstr = ("def %(key)s%(pl1)s: return _getpen().%(key)s%(pl2)s" % + {'key':methodname, 'pl1':pl1, 'pl2':pl2}) + exec(defstr) + eval(methodname).__doc__ = _turtle_docrevise(eval('Turtle.'+methodname).__doc__) + + +done = mainloop + +if __name__ == "__main__": + def switchpen(): + if isdown(): + pu() + else: + pd() + + def demo1(): + """Demo of old turtle.py - module""" + reset() + tracer(True) + up() + backward(100) + down() + # draw 3 squares; the last filled + width(3) + for i in range(3): + if i == 2: + begin_fill() + for _ in range(4): + forward(20) + left(90) + if i == 2: + color("maroon") + end_fill() + up() + forward(30) + down() + width(1) + color("black") + # move out of the way + tracer(False) + up() + right(90) + forward(100) + right(90) + forward(100) + right(180) + down() + # some text + write("startstart", 1) + write("start", 1) + color("red") + # staircase + for i in range(5): + forward(20) + left(90) + forward(20) + right(90) + # filled staircase + tracer(True) + begin_fill() + for i in range(5): + forward(20) + left(90) + forward(20) + right(90) + end_fill() + # more text + + def demo2(): + """Demo of some new features.""" + speed(1) + st() + pensize(3) + setheading(towards(0, 0)) + radius = distance(0, 0)/2.0 + rt(90) + for _ in range(18): + switchpen() + circle(radius, 10) + write("wait a moment...") + while undobufferentries(): + undo() + reset() + lt(90) + colormode(255) + laenge = 10 + pencolor("green") + pensize(3) + lt(180) + for i in range(-2, 16): + if i > 0: + begin_fill() + fillcolor(255-15*i, 0, 15*i) + for _ in range(3): + fd(laenge) + lt(120) + end_fill() + laenge += 10 + lt(15) + speed((speed()+1)%12) + #end_fill() + + lt(120) + pu() + fd(70) + rt(30) + pd() + color("red","yellow") + speed(0) + begin_fill() + for _ in range(4): + circle(50, 90) + rt(90) + fd(30) + rt(90) + end_fill() + lt(90) + pu() + fd(30) + pd() + shape("turtle") + + tri = getturtle() + tri.resizemode("auto") + turtle = Turtle() + turtle.resizemode("auto") + turtle.shape("turtle") + turtle.reset() + turtle.left(90) + turtle.speed(0) + turtle.up() + turtle.goto(280, 40) + turtle.lt(30) + turtle.down() + turtle.speed(6) + turtle.color("blue","orange") + turtle.pensize(2) + tri.speed(6) + setheading(towards(turtle)) + count = 1 + while tri.distance(turtle) > 4: + turtle.fd(3.5) + turtle.lt(0.6) + tri.setheading(tri.towards(turtle)) + tri.fd(4) + if count % 20 == 0: + turtle.stamp() + tri.stamp() + switchpen() + count += 1 + tri.write("CAUGHT! ", font=("Arial", 16, "bold"), align="right") + tri.pencolor("black") + tri.pencolor("red") + + def baba(xdummy, ydummy): + clearscreen() + bye() + + time.sleep(2) + + while undobufferentries(): + tri.undo() + turtle.undo() + tri.fd(50) + tri.write(" Click me!", font = ("Courier", 12, "bold") ) + tri.onclick(baba, 1) + + demo1() + demo2() + exitonclick() diff --git a/webpython/webpython.py b/webpython/webpython.py new file mode 100644 index 00000000..350f3755 --- /dev/null +++ b/webpython/webpython.py @@ -0,0 +1,174 @@ +#!/usr/bin/python3 +# Main interpreter entry for webpython +import io, select, sys, os, threading, code +import pickle, struct, builtins, json +#, ressource +from queue import Queue + +# hard limit to 64M +#try: +# resource.setrlimit(resource.RLIMIT_AS, (1<<26, 1<<26)) +#except ValueError: + # tried to raise it +# pass + +# output limit 16MiB +output_capacity = 16*1024*1024 +# adapted from IDLE (PyShell.py) +class PseudoFile(io.TextIOBase): + + def __init__(self, shell, name): + self.shell = shell + self._name = name + + @property + def encoding(self): + return "UTF-8" + + @property + def name(self): + return '<%s>' % self._name + + def isatty(self): + return True + +class PseudoInputFile(PseudoFile): + + def __init__(self, shell, name): + PseudoFile.__init__(self, shell, name) + self._line_buffer = '' + + def readable(self): + return True + + def read(self, size=-1): + if self.closed: + raise ValueError("read from closed file") + if size is None: + size = -1 + elif not isinstance(size, int): + raise TypeError('must be int, not ' + type(size).__name__) + result = self._line_buffer + self._line_buffer = '' + if size < 0: + while True: + line = self.shell.readline() + if not line: break + result += line + else: + while len(result) < size: + line = self.shell.readline() + if not line: break + result += line + self._line_buffer = result[size:] + result = result[:size] + return result + + def readline(self, size=-1): + if self.closed: + raise ValueError("read from closed file") + if size is None: + size = -1 + elif not isinstance(size, int): + raise TypeError('must be int, not ' + type(size).__name__) + line = self._line_buffer or self.shell.readline() + if size < 0: + size = len(line) + self._line_buffer = line[size:] + return line[:size] + + def close(self): + self.shell.close() + +class PseudoOutputFile(PseudoFile): + + def writable(self): + return True + + def write(self, s): + if self.closed: + raise ValueError("write to closed file") + if not isinstance(s, str): + raise TypeError('must be str, not ' + type(s).__name__) + return self.shell.write(s, self._name) + +# RPC proxy +orig_stdin = sys.stdin +orig_stdout = sys.stdout +orig_stderr = sys.stderr +class Shell: + def __init__(self): + self.stdin = io.FileIO(0) + self.buf = b'' + self.canvas = [] + self.messages = [] + self.capacity = output_capacity + + # PseudoFile interaction + #def readline(self): + # self.sendpickle({'cmd':'readline', + # 'stream':'stdin', + # }) + # return self.inputq.get() + + def write(self, data, name): + self.sendpickle({'cmd':'write', + 'stream':name, + 'data':data + }) + + def input(self, prompt=''): + self.sendpickle({'cmd':'input', + 'stream':'stdin', + 'data':prompt}) + result = self.receivemsg() + return result['data'] + + # internal + def sendpickle(self, data): + data = json.dumps(data) + "\n\r" + self.capacity -= len(data) + if self.capacity < 0: + data = json.dumps({'cmd':'stop', + 'timedout':True}, 2) + orig_stdout.write(data) + raise SystemExit + orig_stdout.write(data) + + def receivepickle(self): + msg = json.loads(orig_stdin.readline()) + if msg['cmd'] == 'canvasevent': + self.canvas.append(msg) + else: + self.messages.append(msg) + + def receivemsg(self): + while not self.messages: + self.receivepickle() + return self.messages.pop() + + def receivecanvas(self): + while not self.canvas: + self.receivepickle() + return self.canvas.pop(0) + +# Hide 0/1 from sys +shell = Shell() +sys.__stdin__ = sys.stdin = PseudoInputFile(shell, 'stdin') +sys.__stdout__ = sys.stdout = PseudoOutputFile(shell, 'stdout') +#sys.__stderr__ = sys.stderr = PseudoOutputFile(shell, 'stderr') +builtins.input = shell.input + +#iothread = threading.Thread(target=shell.run) +#iothread.start() + +if __name__ == '__main__': + #script = "print (\"Hello world.\")\n\rmsg = input()\n\rprint(\"Out:\")\n\rprint(msg)" + #with open("programm.py", "w", encoding='utf-8') as f: + # f.write(script) + with open(os.path.join("/", "workspace", "exercise.py"), "r", encoding='utf-8') as f: + script = f.read() + c = compile(script, "exercise.py", 'exec') + exec(c, {}) + # work-around for docker not terminating properly + shell.sendpickle({'cmd':'exit'}) From 76a9443b3e4e359d9cd12c8a0ad92dc169f57ae7 Mon Sep 17 00:00:00 2001 From: Janusch Jacoby Date: Wed, 16 Sep 2015 17:07:09 +0200 Subject: [PATCH 04/21] Fix scoring bug --- lib/docker_client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docker_client.rb b/lib/docker_client.rb index 3b39c71b..3927fe9f 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -198,7 +198,7 @@ class DockerClient execute_websocket_command(command, create_workspace_files, block) end - def execute_test_command(subbmission, filename, &block) + def execute_test_command(submission, filename, &block) """ Stick to existing Docker API with exec command. """ From c451f9a3d011ff678f57aa6c1e61ef7f626af52c Mon Sep 17 00:00:00 2001 From: Janusch Jacoby Date: Wed, 16 Sep 2015 18:04:36 +0200 Subject: [PATCH 05/21] Remove assess.py dependency --- webpython/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/webpython/Dockerfile b/webpython/Dockerfile index 978d68b0..54f1b014 100644 --- a/webpython/Dockerfile +++ b/webpython/Dockerfile @@ -5,7 +5,6 @@ ENV LANG en_US.UTF-8 ADD webpython.py /usr/lib/python3.4/webpython.py RUN rm /usr/lib/python3.4/turtle.py ADD turtle.py /usr/lib/python3.4/turtle.py -ADD assess /usr/lib/python3.4/assess RUN adduser --disabled-password --gecos Python python USER python WORKDIR /home/python From 81aa5d5d8f410c81f873ad603f80d98a9e97bf61 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Wed, 16 Sep 2015 18:34:04 +0200 Subject: [PATCH 06/21] Fixed submission-scoring. Added parsing of exit cmd. --- app/controllers/submissions_controller.rb | 41 +++++++++++++++++------ lib/docker_client.rb | 33 +++++++++--------- webpython/Dockerfile | 1 - 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index a84f069e..b246de80 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -93,20 +93,40 @@ class SubmissionsController < ApplicationController Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? result = @docker_client.execute_run_command(@submission, params[:filename]) - socket = result[:socket] - socket.on :message do |event| - Rails.logger.info("Docker sending: " + event.data) - handle_message(event.data, tubesock) - end - - socket.on :close do |event| + if result[:status] == :container_depleted + tubesock.send_data JSON.dump({'cmd' => 'container_depleted'}) kill_socket(tubesock) end - tubesock.onmessage do |data| - Rails.logger.info("Client sending: " + data) - socket.send data + if result[:status] == :container_running + socket = result[:socket] + + socket.on :message do |event| + Rails.logger.info("Docker sending: " + event.data) + handle_message(event.data, tubesock) + end + + socket.on :close do |event| + kill_socket(tubesock) + end + + tubesock.onmessage do |data| + Rails.logger.info("Client sending: " + data) + # Check wether the client send a JSON command and kill container + # if the command is 'exit', send it to docker otherwise. + begin + parsed = JSON.parse(data) + if parsed['cmd'] == 'exit' + Rails.logger.info("Client killed container.") + @docker_client.kill_container(result[:container]) + else + socket.send data + end + rescue JSON::ParserError + socket.send data + end + end end end end @@ -134,6 +154,7 @@ class SubmissionsController < ApplicationController parsed = JSON.parse(message) socket.send_data message rescue JSON::ParserError => e + # Check wether the message contains multiple lines, if true try to parse each line if ((recursive == true) && (message.include? "\n")) for part in message.split("\n") self.parse_message(part,output_stream,socket,false) diff --git a/lib/docker_client.rb b/lib/docker_client.rb index 3b39c71b..1c0de144 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -155,7 +155,7 @@ class DockerClient @socket ||= create_socket(@container) # Newline required to flush @socket.send command + "\n" - {status: :container_running, socket: @socket} + {status: :container_running, socket: @socket, container: @container} else {status: :container_depleted} end @@ -170,22 +170,25 @@ class DockerClient timeout = @execution_environment.permitted_execution_time.to_i # seconds sleep(timeout) Rails.logger.info("Killing container after timeout of " + timeout.to_s + " seconds.") - # if we use pooling and recylce the containers, put it back. otherwise, destroy it. - # (DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container) + kill_container(container) + end + end - # todo won't this always create a new container? - # remove container from pool, then destroy it - (DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) : + def kill_container(container) - # destroy container - self.class.destroy_container(container) + # todo won't this always create a new container? + # It does, because it's impossible to determine wether a programm is still running or not while using ws to run. + # remove container from pool, then destroy it + (DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) : - # if we recylce containers, we start a fresh one - if(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) - # create new container and add it to @all_containers and @containers. - container = self.class.create_container(@execution_environment) - DockerContainerPool.add_to_all_containers(container, @execution_environment) - end + #destroy container + self.class.destroy_container(container) + + # if we recylce containers, we start a fresh one + if(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) + # create new container and add it to @all_containers and @containers. + container = self.class.create_container(@execution_environment) + DockerContainerPool.add_to_all_containers(container, @execution_environment) end end @@ -198,7 +201,7 @@ class DockerClient execute_websocket_command(command, create_workspace_files, block) end - def execute_test_command(subbmission, filename, &block) + def execute_test_command(submission, filename, &block) """ Stick to existing Docker API with exec command. """ diff --git a/webpython/Dockerfile b/webpython/Dockerfile index 978d68b0..54f1b014 100644 --- a/webpython/Dockerfile +++ b/webpython/Dockerfile @@ -5,7 +5,6 @@ ENV LANG en_US.UTF-8 ADD webpython.py /usr/lib/python3.4/webpython.py RUN rm /usr/lib/python3.4/turtle.py ADD turtle.py /usr/lib/python3.4/turtle.py -ADD assess /usr/lib/python3.4/assess RUN adduser --disabled-password --gecos Python python USER python WORKDIR /home/python From ec570b32261cd79a1a4735b4fb31258f42c04ea4 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Wed, 16 Sep 2015 18:35:47 +0200 Subject: [PATCH 07/21] Added .idea to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b9427dff..af4940cf 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ /tmp /vagrant/ *.sublime-* +/.idea \ No newline at end of file From 13be0f65dd265dd6878ccd477181bf3891e53e7b Mon Sep 17 00:00:00 2001 From: Janusch Jacoby Date: Wed, 16 Sep 2015 19:14:36 +0200 Subject: [PATCH 08/21] Improve error and output handling Display websocket and container status messages as well as line feeds to the user. --- app/assets/javascripts/editor.js | 129 +++++++++------------- app/controllers/submissions_controller.rb | 10 +- lib/docker_client.rb | 10 +- 3 files changed, 61 insertions(+), 88 deletions(-) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 838b17a2..6d505efc 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -25,7 +25,7 @@ $(function() { numMessages = 0, turtlecanvas = $('#turtlecanvas'), prompt = $('#prompt'), - commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit'], + commands = ['input', 'write', 'turtle', 'turtlebatch', 'exit', 'status'], streams = ['stdin', 'stdout', 'stderr']; var ENTER_KEY_CODE = 13; @@ -192,20 +192,14 @@ $(function() { var evaluateCodeWithStreamedResponse = function(url, callback) { initWebsocketConnection(url); - console.log(callback); // TODO only init turtle when required initTurtle(); // TODO reimplement via websocket messsages /*var event_source = new EventSource(url); - - event_source.addEventListener('close', closeEventSource); - event_source.addEventListener('error', closeEventSource); event_source.addEventListener('hint', renderHint); event_source.addEventListener('info', storeContainerInformation); - event_source.addEventListener('output', callback); - event_source.addEventListener('start', callback); if ($('#flowrHint').isPresent()) { event_source.addEventListener('output', handleStderrOutputForFlowr); @@ -214,11 +208,7 @@ $(function() { if (qa_api) { event_source.addEventListener('close', handleStreamedResponseForCodePilot); - } - - event_source.addEventListener('status', function(event) { - showStatus(JSON.parse(event.data)); - });*/ + }*/ }; var handleStreamedResponseForCodePilot = function(event) { @@ -976,7 +966,7 @@ $(function() { if (output.status === 'timeout') { showTimeoutMessage(); } else if (output.status === 'container_depleted') { - showContainerDepletedMessage(); + showContainerDepletedMessage(); } else if (output.stderr) { $.flash.danger({ icon: ['fa', 'fa-bug'], @@ -1010,6 +1000,12 @@ $(function() { }); }; + var showWebsocketError = function() { + $.flash.danger({ + text: $('#flash').data('message-failure') + }); + } + var showWorkspaceTab = function(event) { event.preventDefault(); showTab(1); @@ -1018,40 +1014,29 @@ $(function() { var stopCode = function(event) { event.preventDefault(); if ($('#stop').is(':visible')) { - killWebsocket(); - stopContainer(); + killWebsocketAndContainer(); } }; - // todo we are missing the url here - // we could also hide the container completely by killing it on the server and only exposing the websocket - var stopContainer = function() { - var jqxhr = ajax({ - data: { - container_id: $('#stop').data('container').id - }, - url: $('#stop').data('url') - }); - jqxhr.always(function() { - hideSpinner(); - running = false; - toggleButtonStates(); - }); - jqxhr.fail(ajaxError); - } - - var killWebsocket = function() { + var killWebsocketAndContainer = function() { if (websocket.readyState != WebSocket.OPEN) { return; } - // todo flash notification websocket.send(JSON.stringify({cmd: 'exit'})); websocket.flush(); websocket.close(); - // todo remove this once xhr works or container is killed on the server hideSpinner(); running = false; toggleButtonStates(); + hidePrompt(); + flashKillMessage(); + } + + var flashKillMessage = function() { + $.flash.info({ + icon: ['fa', 'fa-clock-o'], + text: "Your program was stopped." // todo get data attribute + }); } // todo set this from websocket command, required to e.g. stop container @@ -1111,10 +1096,10 @@ $(function() { var initWebsocketConnection = function(url) { websocket = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + url); - websocket.onopen = function(evt) { onWebSocketOpen(evt) }; - websocket.onclose = function(evt) { onWebSocketClose(evt) }; - websocket.onmessage = function(evt) { onWebSocketMessage(evt) }; - websocket.onerror = function(evt) { onWebSocketError(evt) }; + websocket.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection + websocket.onclose = function(evt) { /* expected at some point */ }; + websocket.onmessage = function(evt) { parseCanvasMessage(evt.data, true); }; + websocket.onerror = function(evt) { showWebsocketError(); }; websocket.flush = function() { this.send('\n'); } }; @@ -1136,51 +1121,42 @@ $(function() { } } - var onWebSocketOpen = function(evt) { - resetOutputTab(); - }; - - var onWebSocketClose = function(evt) { - // no reason to alert since this will happen either way - }; - - var onWebSocketMessage = function(evt) { - parseCanvasMessage(evt.data, true); - }; - - var onWebSocketError = function(evt) { - // todo flash error message - }; - var executeWebsocketCommand = function(msg) { if ($.inArray(msg.cmd, commands) == -1) { - console.log("Unknown command: " + msg.cmd); - // skipping unregistered commands is required - // as we may receive mirrored response due to internal behaviour - return; + console.log("Unknown command: " + msg.cmd); + // skipping unregistered commands is required + // as we may receive mirrored response due to internal behaviour + return; } switch(msg.cmd) { - case 'input': - showPrompt(); - break; - case 'write': - printWebsocketOutput(msg); - break; - case 'turtle': - showCanvas(); - handleTurtleCommand(msg); - break; - case 'turtlebatch': - showCanvas(); - handleTurtlebatchCommand(msg); - break; - case 'exit': - killWebsocket(); - break; + case 'input': + showPrompt(); + break; + case 'write': + printWebsocketOutput(msg); + break; + case 'turtle': + showCanvas(); + handleTurtleCommand(msg); + break; + case 'turtlebatch': + showCanvas(); + handleTurtlebatchCommand(msg); + break; + case 'exit': + killWebsocketAndContainer(); + break; + case 'status': + showStatus(msg) + break; } }; var printWebsocketOutput = function(msg) { + if (!msg.data) { + return; + } + msg.data = msg.data.replace(/(\r\n|\n|\r)/gm, "
"); var stream = {}; stream[msg.stream] = msg.data; printOutput(stream, true, 0); @@ -1210,7 +1186,6 @@ $(function() { } var submitPromptInput = function() { - // todo make sure websocket is actually open var input = $('#prompt-input'); var message = input.val(); websocket.send(JSON.stringify({cmd: 'result', 'data': message})); diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index b246de80..b7940ed9 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -78,8 +78,6 @@ class SubmissionsController < ApplicationController # server_sent_event.write({stdout: output[:stdout]}, event: 'output') if output[:stdout] # server_sent_event.write({stderr: output[:stderr]}, event: 'output') if output[:stderr] - # server_sent_event.write({status: output[:status]}, event: 'status') - # unless output[:stderr].nil? # if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(output[:stderr]) # server_sent_event.write(hint, event: 'hint') @@ -93,11 +91,7 @@ class SubmissionsController < ApplicationController Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? result = @docker_client.execute_run_command(@submission, params[:filename]) - - if result[:status] == :container_depleted - tubesock.send_data JSON.dump({'cmd' => 'container_depleted'}) - kill_socket(tubesock) - end + tubesock.send_data JSON.dump({'cmd' => 'status', 'status' => result[:status]}) if result[:status] == :container_running socket = result[:socket] @@ -127,6 +121,8 @@ class SubmissionsController < ApplicationController socket.send data end end + else + kill_socket(tubesock) end end end diff --git a/lib/docker_client.rb b/lib/docker_client.rb index 1c0de144..3562da13 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -164,7 +164,7 @@ class DockerClient def kill_after_timeout(container) """ We need to start a second thread to kill the websocket connection, - as it is impossible to determine when no more input is requested. + as it is impossible to determine whether further input is requested. """ Thread.new do timeout = @execution_environment.permitted_execution_time.to_i # seconds @@ -175,9 +175,11 @@ class DockerClient end def kill_container(container) - - # todo won't this always create a new container? - # It does, because it's impossible to determine wether a programm is still running or not while using ws to run. + """ + Please note that we cannot properly recycle containers when using + websockets because it is impossible to determine whether a program + is still running. + """ # remove container from pool, then destroy it (DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) : From 13a620760229019af8c106036acd98ea0b5cca7f Mon Sep 17 00:00:00 2001 From: Janusch Jacoby Date: Mon, 21 Sep 2015 16:46:46 +0200 Subject: [PATCH 09/21] Make tests work --- webpython/Dockerfile | 3 +- webpython/assess.py | 234 +++++++++++++++++++++++++++++++++++++++++ webpython/webpython.py | 14 ++- 3 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 webpython/assess.py diff --git a/webpython/Dockerfile b/webpython/Dockerfile index 54f1b014..76eae9dc 100644 --- a/webpython/Dockerfile +++ b/webpython/Dockerfile @@ -2,9 +2,10 @@ FROM ubuntu:14.04 MAINTAINER "Martin v. Löwis" RUN locale-gen en_US.UTF-8 ENV LANG en_US.UTF-8 +ADD assess.py /usr/lib/python3.4/assess.py ADD webpython.py /usr/lib/python3.4/webpython.py RUN rm /usr/lib/python3.4/turtle.py ADD turtle.py /usr/lib/python3.4/turtle.py RUN adduser --disabled-password --gecos Python python USER python -WORKDIR /home/python +WORKDIR /usr/lib/python3.4 diff --git a/webpython/assess.py b/webpython/assess.py new file mode 100644 index 00000000..b14a13b9 --- /dev/null +++ b/webpython/assess.py @@ -0,0 +1,234 @@ +import webpython, contextlib, io, json, sys, turtle, ast +from webpython import shell + +turtle_operations = [] +bindings = {} + +class RecordingPen: + _pen = None + _screen = None + def __init__(self): + self.operations = turtle_operations + self._pos = (0,0) + turtle_operations.append(('__init__',())) + + def reset(self): + turtle_operations.clear() + + def onclick(self, fun, btn=1, add=None): + self.operations.append(('onclick', (fun,))) + def eventfun(event): + fun(event.x, event.y) + bindings[''] = eventfun + + def goto(self, x, y): + self._pos = (x,y) + self.operations.append(('goto', (x,y))) + + def pos(self): + self.operations.append(('pos', ())) + return self._pos + + def __getattr__(self, method): + def func(*args): + self.operations.append((method, args)) + return func + +class FakeCanvas(turtle.WebCanvas): + def flushbatch(self): + pass + + def get_width(self): + return 400 + + def get_height(self): + return 400 + + def delete(self, item): + pass + + def css(self, key, value): + pass + +fake_events = [] +def mainloop(): + while fake_events: + e = turtle.Event(fake_events.pop(0)) + if e.type in bindings: + bindings[e.type](e) + +turtle.Turtle = RecordingPen +turtle.WebCanvas = FakeCanvas +pen = turtle._getpen() +turtle.mainloop = mainloop + +def filter_operations(name): + return [o for o in turtle_operations if o[0] == name] + +@contextlib.contextmanager +def capture(): + global captured_out + import sys + oldout,olderr = sys.stdout, sys.stderr + try: + out=[io.StringIO(), io.StringIO()] + captured_out = out + sys.stdout,sys.stderr = out + yield out + finally: + sys.stdout,sys.stderr = oldout, olderr + out[0] = out[0].getvalue() + out[1] = out[1].getvalue() + +def get_source(): + message = json.loads(sys.argv[1]) + return message['data'] + +def get_ast(): + s = get_source() + return ast.parse(s, "programm.py", "exec") + +def has_bare_except(): + for node in ast.walk(get_ast()): + if isinstance(node, ast.ExceptHandler): + if node.type is None: + return True + return False + +def runcaptured(prefix='', tracing=None, variables=None, source=''): + #message = json.loads(sys.argv[1]) + #source = prefix + message['data'] + with open("programm.py", "w", encoding='utf-8') as f: + f.write(source) + c = compile(source, "programm.py", 'exec') + with capture() as out, trace(tracing): + if variables is None: + variables = {} + exec(c, variables) + return source, out[0], out[1], variables + +def runfunc(func, *args, tracing=None): + with capture() as out, trace(tracing): + res = func(*args) + return out[0], out[1], res + +def passed(): + msg_in = json.loads(sys.argv[1]) + msg_out = {'cmd':'passed'} + msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url'] + msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid'] + webpython.shell.sendpickle(msg_out) + +def failed(msg): + msg_in = json.loads(sys.argv[1]) + msg_out = {'cmd':'failed', 'data':'Dein Programm ist leider falsch:\n'+msg} + msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url'] + msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid'] + webpython.shell.sendpickle(msg_out) + +def modified(variables, name, val): + if variables.get(name) != val: + msg_in = json.loads(sys.argv[1]) + msg_out = {'cmd':'failed', + 'data':('Bitte lösche Deine Zuweisung der Variable %s, '+ + 'damit wir Dein Programm überprüfen können.') % name} + msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url'] + msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid'] + webpython.shell.sendpickle(msg_out) + return True + return False + +undefined = object() +def getvar(variables, name): + try: + return variables['name'] + except KeyError: + name = name.lower() + for k,v in variables.items(): + if k.lower() == name: + return v + return undefined + +def _match(n1, n2): + if n1 == n2: + return True + if n1 is None or n2 is None: + return False + return n1.lower() == n2.lower() + +class Call: + def __init__(self, name, args): + self.name = name + self.args = args + self.calls = [] + self.current = None + + def findcall(self, f): + if _match(self.name, f): + return self + for c in self.calls: + r = c.findcall(f) + if r: + return r + return None + + def calling(self, caller, callee): + if _match(self.name, caller): + for c in self.calls: + if _match(c.name, callee): + return True + for c in self.calls: + if c.calling(caller, callee): + return True + return False + + def countcalls(self, caller, callee): + calls = 0 + if _match(self.name, caller): + for c in self.calls: + if _match(c.name, callee): + calls += 1 + return calls + for c in self.calls: + r = c.countcalls(caller, callee) + if r > 0: + return r + return 0 + +class Tracing(Call): + def __init__(self): + Call.__init__(self, None, None) + + def trace(self, frame, event, arg): + if event == 'call': + c = Call(frame.f_code.co_name, frame.f_locals.copy()) + cur = self + while cur.current: + cur = cur.current + cur.calls.append(c) + cur.current = c + return self.trace + elif event in ('return', 'exception'): + cur = self + if not cur.current: + # XXX return without call? happens when invocation of top function fails + return + while cur.current.current: + cur = cur.current + cur.current = None + + def start(self): + sys.settrace(self.trace) + + def stop(self): + sys.settrace(None) + +@contextlib.contextmanager +def trace(t): + try: + if t: + t.start() + yield + finally: + if t: + t.stop() diff --git a/webpython/webpython.py b/webpython/webpython.py index 350f3755..b81ac680 100644 --- a/webpython/webpython.py +++ b/webpython/webpython.py @@ -4,6 +4,7 @@ import io, select, sys, os, threading, code import pickle, struct, builtins, json #, ressource from queue import Queue +from argparse import ArgumentParser # hard limit to 64M #try: @@ -163,12 +164,15 @@ builtins.input = shell.input #iothread.start() if __name__ == '__main__': - #script = "print (\"Hello world.\")\n\rmsg = input()\n\rprint(\"Out:\")\n\rprint(msg)" - #with open("programm.py", "w", encoding='utf-8') as f: - # f.write(script) - with open(os.path.join("/", "workspace", "exercise.py"), "r", encoding='utf-8') as f: + parser = ArgumentParser(description='A python interpreter that generates json commands based on the standard I/O streams.') + parser.add_argument('-f', '--filename', type=str, required=True, default='exercise.py', help='Python file to be interpreted.') + args = parser.parse_args() + + filepath = os.path.join("/", "workspace", args.filename) + with open(filepath, "r", encoding='utf-8') as f: script = f.read() - c = compile(script, "exercise.py", 'exec') + c = compile(script, args.filename, 'exec') exec(c, {}) + # work-around for docker not terminating properly shell.sendpickle({'cmd':'exit'}) From 4e35579a7d561ea8c779e7ade9d30e45edf21a6c Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Mon, 21 Sep 2015 18:14:40 +0200 Subject: [PATCH 10/21] fixed the tests by deleting the root of all evil: the 's'. --- lib/py_unit_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/py_unit_adapter.rb b/lib/py_unit_adapter.rb index 29a47ddf..0e80c593 100644 --- a/lib/py_unit_adapter.rb +++ b/lib/py_unit_adapter.rb @@ -1,5 +1,5 @@ class PyUnitAdapter < TestingFrameworkAdapter - COUNT_REGEXP = /Ran (\d+) tests/ + COUNT_REGEXP = /Ran (\d+) test/ FAILURES_REGEXP = /FAILED \(failures=(\d+)\)/ def self.framework_name From 10da12f9501cd72303a4ef6543d76e703fdd2dc4 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 22 Sep 2015 18:22:25 +0200 Subject: [PATCH 11/21] option to hide the file tree in exercises. --- app/controllers/exercises_controller.rb | 2 +- app/views/exercises/_editor.html.slim | 4 ++-- app/views/exercises/_form.html.slim | 4 ++++ app/views/exercises/implement.html.slim | 11 ++++------- app/views/exercises/show.html.slim | 1 + config/locales/de.yml | 1 + config/locales/en.yml | 1 + db/schema.rb | 3 ++- 8 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index f2d3390c..7a26627a 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -62,7 +62,7 @@ class ExercisesController < ApplicationController end def exercise_params - params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) + params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) end private :exercise_params diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 986af92d..cc5dfe7e 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,6 +1,6 @@ #editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id - .col-sm-3 = render('editor_file_tree', files: @files) - #frames.col-sm-9 + div class=(@exercise.hide_file_tree ? 'hidden col-sm-3' : 'col-sm-3') = render('editor_file_tree', files: @files) + div id='frames' class=(@exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9') - @files.each do |file| = render('editor_frame', exercise: exercise, file: file) #autosave-label diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index 32365e4a..1002f78a 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -23,6 +23,10 @@ label = f.check_box(:public) = t('activerecord.attributes.exercise.public') + .checkbox + label + = f.check_box(:hide_file_tree) + = t('activerecord.attributes.exercise.hide_file_tree') h2 = t('activerecord.attributes.exercise.files') ul#files.list-unstyled = f.fields_for :files do |files_form| diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index b0a3f0e0..85557b81 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -13,19 +13,15 @@ #development-environment ul.nav.nav-justified.nav-tabs role='tablist' li.active - a data-placement='top' data-toggle='tab' data-tooltip=true href='#instructions' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 1') - i.fa.fa-question - = t('activerecord.attributes.exercise.instructions') - li - a data-placement='top' data-toggle='tab' data-tooltip=true href='#workspace' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 2') + a data-placement='top' data-toggle='tab' data-tooltip=true href='#workspace' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 1') i.fa.fa-code = t('.workspace') li - a data-placement='top' data-toggle='tab' data-tooltip=true href='#outputInformation' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 3') + a data-placement='top' data-toggle='tab' data-tooltip=true href='#outputInformation' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 2') i.fa.fa-terminal = t('.output') li - a data-placement='top' data-toggle='tab' data-tooltip=true href='#progress' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 4') + a data-placement='top' data-toggle='tab' data-tooltip=true href='#progress' role='tab' title=t('shared.tooltips.shortcut', shortcut: 'ALT + 3') i.fa.fa-line-chart = t('.progress') @@ -46,6 +42,7 @@ .panel-heading = t('.hint') .panel-body .row + / #output-col1.col-sm-12 #output-col1 // todo set to full width if turtle isnt used #prompt.input-group.hidden diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index 6456c3c0..67185046 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -14,6 +14,7 @@ h1 = row(label: 'exercise.team', value: @exercise.team ? link_to(@exercise.team, @exercise.team) : nil) = row(label: 'exercise.maximum_score', value: @exercise.maximum_score) = row(label: 'exercise.public', value: @exercise.public?) += row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?) = row(label: 'exercise.embedding_parameters') do = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) diff --git a/config/locales/de.yml b/config/locales/de.yml index cb8ef305..9cb546b3 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -28,6 +28,7 @@ de: execution_environment: Ausführungsumgebung execution_environment_id: Ausführungsumgebung files: Dateien + hide_file_tree: Dateibaum verstecken instructions: Anweisungen maximum_score: Erreichbare Punktzahl public: Öffentlich diff --git a/config/locales/en.yml b/config/locales/en.yml index e726557d..d7fd84a9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -28,6 +28,7 @@ en: execution_environment: Execution Environment execution_environment_id: Execution Environment files: Files + hide_file_tree: Hide File Tree instructions: Instructions maximum_score: Maximum Score public: Public diff --git a/db/schema.rb b/db/schema.rb index 7f3be339..d895da3f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150903152727) do +ActiveRecord::Schema.define(version: 20150922125415) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -79,6 +79,7 @@ ActiveRecord::Schema.define(version: 20150903152727) do t.string "user_type" t.string "token" t.integer "team_id" + t.boolean "hide_file_tree" end create_table "external_users", force: true do |t| From caf47a92513f6a5cfb3848adcd056528a50cb76b Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 25 Sep 2015 11:56:04 +0200 Subject: [PATCH 12/21] correct tab shortcuts (following the removal of the introduction tab) --- app/assets/javascripts/editor.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 6d505efc..25767031 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -59,7 +59,7 @@ $(function() { if (event.type === 'error' || JSON.parse(event.data).code !== 200) { ajaxError(); - showTab(1); + showTab(0); } }; @@ -262,13 +262,11 @@ $(function() { var handleKeyPress = function(event) { if (event.which === ALT_1_KEY_CODE) { - showTab(0); - } else if (event.which === ALT_2_KEY_CODE) { showWorkspaceTab(event); + } else if (event.which === ALT_2_KEY_CODE) { + showTab(1); } else if (event.which === ALT_3_KEY_CODE) { showTab(2); - } else if (event.which === ALT_4_KEY_CODE) { - showTab(3); } else if (event.which === ALT_R_KEY_CODE) { $('#run').trigger('click'); } else if (event.which === ALT_S_KEY_CODE) { @@ -311,7 +309,7 @@ $(function() { }, 0).toFixed(2); $('#score').data('score', score); renderScore(); - showTab(3); + showTab(2); }; var stderrOutput = ''; @@ -364,7 +362,7 @@ $(function() { qa_api.executeCommand('syncOutput', [response]); } showStatus(response[0]); - showTab(2); + showTab(1); }; var hideSpinner = function() { @@ -719,7 +717,7 @@ $(function() { clearOutput(); $('#hint').fadeOut(); $('#flowrHint').fadeOut(); - showTab(2); + showTab(1); } var printOutput = function(output, colorize, index) { @@ -820,7 +818,7 @@ $(function() { stderr: message }, true, 0); sendError(message, response.id); - showTab(2); + showTab(1); }; } }); @@ -943,7 +941,7 @@ $(function() { var showOutput = function(event) { event.preventDefault(); - showTab(2); + showTab(1); $('#output').scrollTo($(this).attr('href')); }; @@ -1008,7 +1006,7 @@ $(function() { var showWorkspaceTab = function(event) { event.preventDefault(); - showTab(1); + showTab(0); }; var stopCode = function(event) { From 632b41da236e3b6c5e7e8065f45265c4814c79f6 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 25 Sep 2015 12:13:08 +0200 Subject: [PATCH 13/21] editor has an option whether it should remember the last used tab and show it, or whether it should display the first tab. currently, it shows the first tab. (as users whished for this behaviour) --- app/assets/javascripts/editor.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 25767031..9d866998 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -11,6 +11,7 @@ $(function() { var FILENAME_URL_PLACEHOLDER = '{filename}'; var SUCCESSFULL_PERCENTAGE = 90; var THEME = 'ace/theme/textmate'; + var REMEMBER_TAB = false; var AUTOSAVE_INTERVAL = 15 * 1000; var editors = []; @@ -946,11 +947,16 @@ $(function() { }; var showRequestedTab = function() { - var regexp = /tab=(\d+)/; - if (regexp.test(window.location.search)) { - var index = regexp.exec(window.location.search)[1] - 1; + if(REMEMBER_TAB){ + var regexp = /tab=(\d+)/; + if (regexp.test(window.location.search)) { + var index = regexp.exec(window.location.search)[1] - 1; + } else { + var index = localStorage.tab; + } } else { - var index = localStorage.tab; + // else start with first tab. + var index = 0; } showTab(index); }; From 6672491bc3c0936993aaadee871a89b182380c73 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 25 Sep 2015 12:19:13 +0200 Subject: [PATCH 14/21] commented out translation to line breaks, as they seem not to be needed (and corrected the linebreak tag), thanks to martin for hinting at this. Discuss with janusch why he needed this. --- app/assets/javascripts/editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 9d866998..279e7c27 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -1160,7 +1160,7 @@ $(function() { if (!msg.data) { return; } - msg.data = msg.data.replace(/(\r\n|\n|\r)/gm, "
"); + //msg.data = msg.data.replace(/(\r\n|\n|\r)/gm, "
"); var stream = {}; stream[msg.stream] = msg.data; printOutput(stream, true, 0); From 3657fe3e9363b5c7055bcfa1a36d58687c252d49 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 25 Sep 2015 12:52:52 +0200 Subject: [PATCH 15/21] remove instructions tab (the actual tab, before I just removed the header), set the turtle canvas to have a size of 400x400. --- app/views/exercises/implement.html.slim | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 85557b81..72ac1b2c 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -28,14 +28,7 @@ hr .tab-content - #instructions.tab-pane.active - p = render_markdown(@exercise.instructions) - br - p.text-center - a#start.btn.btn-lg.btn-success - i.fa.fa-code - = t('.start') - #workspace.tab-pane = render('editor', exercise: @exercise, files: @files, submission: @submission) + #workspace.tab-pane.active = render('editor', exercise: @exercise, files: @files, submission: @submission) #outputInformation.tab-pane data-message-no-output=t('.no_output') #hint .panel.panel-warning @@ -59,7 +52,7 @@ #output-col2.col-lg-5.col-md-5 #turtlediv // todo what should the canvas default size be? - canvas#turtlecanvas.hidden style='border-style:solid;border-width:thin' + canvas#turtlecanvas.hidden width=400 height=400 style='border-style:solid;border-width:thin' #progress.tab-pane #results h2 = t('.results') From 56f2cc221a970b67e666e762630ff287a1b802e8 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 25 Sep 2015 12:53:45 +0200 Subject: [PATCH 16/21] filter and hide run_command and test_command in websocket message --- app/controllers/submissions_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index b7940ed9..a1a804f1 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -138,8 +138,10 @@ class SubmissionsController < ApplicationController if (/^exit/.match(message)) kill_socket(tubesock) else - # Filter out information about user and working directory - if !(/root|workspace/.match(message)) + # Filter out information about run_command, test_command, user or working directory + run_command = @submission.execution_environment.run_command + test_command = @submission.execution_environment.test_command + if !(/root|workspace|#{run_command}|#{test_command}/.match(message)) parse_message(message, 'stdout', tubesock) end end From 5da6e74c1c55a9c102b65c6385b5b156077c6aeb Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 8 Oct 2015 18:57:48 +0200 Subject: [PATCH 17/21] forgot to push the according migration to hiding the file tree... --- db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb diff --git a/db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb b/db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb new file mode 100644 index 00000000..9890c35b --- /dev/null +++ b/db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb @@ -0,0 +1,5 @@ +class AddHideFileTreeToExercises < ActiveRecord::Migration + def change + add_column :exercises, :hide_file_tree, :boolean + end +end From 3b34be818cf18667e27583bbafc4a2befdf39d35 Mon Sep 17 00:00:00 2001 From: Tom Staubitz Date: Wed, 14 Oct 2015 18:44:07 +0200 Subject: [PATCH 18/21] some paths added nad fixed --- config/docker.yml.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/docker.yml.erb b/config/docker.yml.erb index cf38ac79..b44497fd 100644 --- a/config/docker.yml.erb +++ b/config/docker.yml.erb @@ -7,7 +7,9 @@ default: &default development: <<: *default host: tcp://192.168.59.104:2376 - workspace_root: <%= File.join('/', 'shared', Rails.env) %> + ws_host: ws://192.168.59.104:2376 + #workspace_root: <%= File.join('/', 'shared', Rails.env) %> + workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %> production: <<: *default From f1067feb3661f1010ed6cf1073f09aad3a5ec3b9 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 14 Oct 2015 19:08:43 +0200 Subject: [PATCH 19/21] push gemfile.lock for test execution --- Gemfile.lock | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6253088e..812bdaf2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -50,8 +50,6 @@ GEM bootstrap-will_paginate (0.0.10) will_paginate builder (3.2.2) - byebug (4.0.5) - columnize (= 0.9.0) capistrano (3.3.5) capistrano-stats (~> 1.1.0) i18n @@ -96,7 +94,6 @@ GEM execjs coffee-script-source (1.9.1) colorize (0.7.7) - columnize (0.9.0) concurrent-ruby (0.8.0) ref (~> 1.0, >= 1.0.5) concurrent-ruby (0.8.0-java) @@ -110,6 +107,7 @@ GEM excon (>= 0.38.0) json erubis (2.7.0) + eventmachine (1.0.8) excon (0.45.2) execjs (2.5.2) factory_girl (4.5.0) @@ -119,6 +117,9 @@ GEM railties (>= 3.0.0) faraday (0.9.1) multipart-post (>= 1.2, < 3) + faye-websocket (0.10.0) + eventmachine (>= 0.12.0) + websocket-driver (>= 0.5.1) ffi (1.9.8) ffi (1.9.8-java) forgery (0.6.0) @@ -302,6 +303,9 @@ GEM thread_safe (0.3.5) thread_safe (0.3.5-java) tilt (1.4.1) + tubesock (0.2.5) + rack (>= 1.5.0) + websocket (>= 1.1.0) turbolinks (2.5.3) coffee-rails tzinfo (1.2.2) @@ -315,6 +319,9 @@ GEM railties (>= 4.0) sprockets-rails (>= 2.0, < 4.0) websocket (1.2.1) + websocket-driver (0.6.2) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) will_paginate (3.0.7) xpath (2.0.0) nokogiri (~> 1.3) @@ -330,7 +337,6 @@ DEPENDENCIES better_errors binding_of_caller bootstrap-will_paginate - byebug capistrano (~> 3.3.0) capistrano-rails capistrano-rvm @@ -345,6 +351,7 @@ DEPENDENCIES database_cleaner docker-api (~> 1.21.1) factory_girl_rails (~> 4.0) + faye-websocket forgery highline ims-lti @@ -375,6 +382,7 @@ DEPENDENCIES sorcery spring thread_safe + tubesock turbolinks uglifier (>= 1.3.0) web-console (~> 2.0) From 91cb2a3391b511a9c94b49dfb80151ac8f550ee7 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 15 Oct 2015 15:27:57 +0200 Subject: [PATCH 20/21] set correct newlines in output --- app/assets/javascripts/editor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 279e7c27..160cf315 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -1161,6 +1161,7 @@ $(function() { return; } //msg.data = msg.data.replace(/(\r\n|\n|\r)/gm, "
"); + msg.data = msg.data.replace(/(\r)/gm, "\n"); var stream = {}; stream[msg.stream] = msg.data; printOutput(stream, true, 0); From 5f76c784114f9fa151d3bf1a4f82b2ffbc69137c Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 15 Oct 2015 15:29:13 +0200 Subject: [PATCH 21/21] add /workspace to PYTHONPATH (must be matched in the CodeOcean constant in docker_client.rb --- webpython/Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webpython/Dockerfile b/webpython/Dockerfile index 76eae9dc..54d6d4ad 100644 --- a/webpython/Dockerfile +++ b/webpython/Dockerfile @@ -2,10 +2,11 @@ FROM ubuntu:14.04 MAINTAINER "Martin v. Löwis" RUN locale-gen en_US.UTF-8 ENV LANG en_US.UTF-8 +ENV PYTHONPATH $PYTHONPATH:/usr/lib/python3.4:/workspace +ENV PATH $PATH:/usr/lib/python3.4 ADD assess.py /usr/lib/python3.4/assess.py ADD webpython.py /usr/lib/python3.4/webpython.py RUN rm /usr/lib/python3.4/turtle.py ADD turtle.py /usr/lib/python3.4/turtle.py RUN adduser --disabled-password --gecos Python python -USER python -WORKDIR /usr/lib/python3.4 +USER python \ No newline at end of file