Sublime Forum

How to access thread results

#1

This is a stripped down version of Will’s tutorial
What I am trying to achieve is to fetch some todos from Jsonplaceholder in a background thread and while that thread fetches the result, an activity indicator is shown and after it’s done, the quick_panel() opens showing the todos.

import json
import sublime
import threading
import urllib.parse
import urllib.request
import sublime_plugin

class GetJsonDataCommand(sublime_plugin.TextCommand):

	def run(self, edit):
		threads = JsonPlaceholder()
		threads.start()
		self.handle_thread(edit, threads)

	def handle_thread(self, edit, threads, offset = 0, i = 0, dir = 1):

		if threads:
			before = i % 8
			after = (7) - before
			if not after:
				dir = -1
			if not before:
				dir = 1
			i += dir
			self.view.set_status('JsonPlacholder', 'Fetching todos [%s=%s]' % \
			(' ' * before, ' ' * after))

			sublime.set_timeout(lambda: self.handle_thread(threads, offset, i, dir), 100)
			return
		self.view.erase_status('JsonPlacholder')
                self.replace(edit, threads)

	def replace(self, edit, thread):
		todos = []
		for todo in thread.result():
			todos.append(todo["title"])
		self.view.window().show_quick_panel(todos, self.on_done)

	def on_done(self, id):
		print(id)


class JsonPlaceholder(threading.Thread):

	def __init__(self):
		self.result = None
		super().__init__()

	def run(self):
		try:
			url = "https://jsonplaceholder.typicode.com/todos"
			req = urllib.request.Request(url)
			req.add_header('User-agent', 'Mozilla/5.0')
			with urllib.request.urlopen(req) as response:
				response_data = response.read().decode("utf-8")
				paste_data = json.loads(response_data)
				self.result = paste_data
				return 
		except urllib.error.HTTPError as error:
			return json.loads(error.read().decode("utf-8"))

		self.result = False

However, I am getting the following error:

AttributeError: 'int' object has no attribute 'result'

I am able to see the indicator in the status bar but unable to access the result of the thread.
My knowledge of threading is quite poor, any help is appreciated.

0 Likes

Concurrency practice for handling plugin I/O requests
#2

There are a few issues with this code. The first is that as presented above, the code doesn’t compile because the self.replace() call in handle_thread() isn’t indented properly, though I’m assuming that should line up with the erase_status() call.

With that change in mind, if you follow the logic of the code as it’s laid out the reason that you’re seeing this error is because the call to self.handle_thread() in the lambda call is broken. Notice that the argument list for handle_thread() is:

def handle_thread(self, edit, threads, offset = 0, i = 0, dir = 1):

However at the call point:

lambda: self.handle_thread(threads, offset, i, dir)

That is, you’ve skipped the edit argument in the call. Thus the result of this call is that every argument you provide is interpreted differently than you expect; edit becomes the value threads, threads becomes the value offset, offset becomes the value i, i becomes the value dir, and dir is set to the default of 1 because it’s not provided.

With that in mind, once the first call to set_timeout() happens and handle_thread() is called again, the value of threads is 0; the if test concludes that it doesn’t need to do anything further, and the code falls through to the self.replace() call, where it provides the value 0 for the argument of threads.

Now it’s easy to see that in replace(), the line in question gets mad because it’s expecting that value to be an instance of JsonPlaceHolder/Thread but it’s actually a number, and kabooom.

The fix in that case would be to include the edit argument as the first argument in the lambda call, so that all of the arguments line up the way you expect them to.

If you make that change, you’ll find that the command never actually completes. That’s because of this line:

if threads:

It looks like the intention is that this should return True while the thread is running and False when it’s not, so that once the request completes the results can be handled. The Thread class doesn’t (AFAIK but I’m no guru) work that way; what this is actually testing is whether or not the value is not None, which it always is.

if you were to include a line like print(threads) at the top of of handle_thread() you can see that the state of the thread changes from started to stopped when the request finishes, but that’s not the state that you’re checking.

So that line should look more like:

if threads.is_alive():

Now the command will correctly drop out of the loop when the result comes back, but there’s a new issue instead:

Traceback (most recent call last):
  File "/home/tmartin/.config/sublime-text-3/Packages/User/test_panel_thing.py", line 28, in <lambda>
    sublime.set_timeout(lambda: self.handle_thread(edit, threads, offset, i, dir), 100)
  File "/home/tmartin/.config/sublime-text-3/Packages/User/test_panel_thing.py", line 33, in handle_thread
    self.replace(edit, threads)
  File "/home/tmartin/.config/sublime-text-3/Packages/User/test_panel_thing.py", line 37, in replace
    for todo in thread.result():
TypeError: 'list' object is not callable

There are a couple of bugs here; the first is that if there was an error collecting the JSON in the thread, it sets the value to False to indicate that, but this code isn’t checking for it. The other is that result isn’t a function, it’s the actual result of the call (which in this case is a list).

Based on that, the code could perhaps be expressed better as the following:

def replace(self, edit, thread):
    if thread.result:
        todos = []
        for todo in thread.result:
            todos.append(todo["title"])
        self.view.window().show_quick_panel(todos, self.on_done)
    else:
        sublime.status_message("Error during request")

Now a test is done to see if there is a result or not; if not a message is displayed in the status bar, but otherwise the result of the command is that a quick panel opens as you might expect:

image

One last thing to note is that in Sublime Text 2 (which is the version the tutorial you’re referencing is from) a plugin was free to create it’s own edit object as desired. However in Sublime Text 3 this is strictly controlled and the only thing that can provide an edit object is Sublime itself, which it passes to the run() method of a TextCommand.

The value of the edit object is only valid from inside of the run() call that it was given to. If you try to keep it and use it later, you’ll get an error as a result.

That doesn’t matter for the code as you have it laid out here because it’s not actually trying to change any text. However if your ultimate goal is to use the result of the call to modify the text, you can no longer do it this way.

Instead, you would need to gather the text and then pass it as an argument to a TextCommand using run_command() so that Sublime will execute that command and it can then make the change for you. Depending on your use case you can use the insert command that already exists to do this, or you can create one of your own.

This is outlined a bit in the plugin porting guide.

3 Likes

#3

Thank you for such a succinct explanation.

0 Likes

#4

For short one-off tasks like this (fetching JSON data from the web), you could also use the “worker” thread. With sublime.set_timeout_async.

Pro: no additional wasteful threads created.
Con: may block for too long, meaning that event listener callbacks like on_activated_async will be invoked somewhat late.

A general clean solution for this would be to have a global running asyncio loop available in plugins.

The pro point would still stand: no additional wasteful thread created.
The con point would disappear, provided we’d use libraries like aiohttp instead of the blocking urllib library.

Essentially, most uses for the threading library in ST would disappear.

0 Likes

#5

I don’t think I understood your explanation. I’m only instantiating the class once. So I’m not creating any extra threads. As for other points, I guess to use async await we will have to await :wink: since I believe that’s not currently possible in ST3.

0 Likes