Repository: locustio/locust

Issue:
SequentialTaskSet is broken when using local class members (headless mode)
<!-- 
If you have a general question about how to use Locust, please check Stack Overflow first https://stackoverflow.com/questions/tagged/locust

You can also ask new questions on SO, https://stackoverflow.com/questions/ask just remember to tag your question with "locust". Do not immediately post your issue here after posting to SO, wait for an answer there instead.

Use this form only for reporting actual bugs in locust. Be mindful that the developers of locust are unpaid volunteers, so make sure you have tried everything you can think of before filing a bug :) 
-->

### Describe the bug
Trying to execute a SequentialTaskSet in headless mode, store result's from first task, and use it in second task

### Expected behavior
Execute first task, retrieve json, store it as dict in SequentialTaskSet object, execute second task and access the variable.

### Actual behavior
Execute first task, json is retrieved, stored. In second task upon trying to access the stored data, I get following exceptions:
1.) AttributeError: 'dict' object has no attribute '_state'
2.) AttributeError: 'dict' object has no attribute 'environment'

### Steps to reproduce
Start provided mockserver.py (flask) on localhost:5000, then run provided locust file (sequence_test.py) with command lines specified



### Environment

- OS: 
Ubuntu 18.04.4 LTS running in WSL
 Linux PSALA059 4.4.0-18990-Microsoft #1-Microsoft Sat Sep 21 17:04:00 PST 2019 x86_64 x86_64 x86_64 GNU/Linux
- Python version:
3.6.9
- Locust version:
locust 1.0.1
- Locust command line that you ran:
run master locust:
` locust -f sequence_test.py --host=http://localhost:5000 --master --expect-workers=1 --headless -u 1 -r 1 -t 1h`

run worker locust:
`locust -f sequence_test.py --worker --master-host=127.0.0.1 --host=http://localhost:5000`

- Locust file contents (anonymized if necessary):
[locust-1.0.1-sequence-bug.zip](https://github.com/locustio/locust/files/4641968/locust-1.0.1-sequence-bug.zip)


Discussion:

Patch:
diff --git a/README.md b/README.md
index fa986ca2..258aa179 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 [![Build Status](https://travis-ci.com/locustio/locust.svg?branch=master)](https://travis-ci.com/locustio/locust)
 [![codecov](https://codecov.io/gh/locustio/locust/branch/master/graph/badge.svg)](https://codecov.io/gh/locustio/locust)
 [![license](https://img.shields.io/github/license/locustio/locust.svg)](https://github.com/locustio/locust/blob/master/LICENSE)
-[![PyPI](https://img.shields.io/pypi/v/locust.svg)](https://pypi.org/project/locust/)
+[![PyPI](https://img.shields.io/pypi/v/locustio.svg)](https://pypi.org/project/locustio/)
 [![PyPI](https://img.shields.io/pypi/pyversions/locust.svg)](https://pypi.org/project/locust/)
 [![GitHub contributors](https://img.shields.io/github/contributors/locustio/locust.svg)](https://github.com/locustio/locust/graphs/contributors)
 
diff --git a/locust/user/task.py b/locust/user/task.py
index e474cba8..7e3d0933 100644
--- a/locust/user/task.py
+++ b/locust/user/task.py
@@ -218,25 +218,19 @@ class TaskSet(object, metaclass=TaskSetMeta):
     if not set on the TaskSet.
     """
 
-    user = None
-    """Will refer to the root User class instance when the TaskSet has been instantiated"""
-
-    parent = None
-    """
-    Will refer to the parent TaskSet, or User, class instance when the TaskSet has been 
-    instantiated. Useful for nested TaskSet classes.
-    """
+    _user = None
+    _parent = None
 
     def __init__(self, parent):
         self._task_queue = []
         self._time_start = time()
         
         if isinstance(parent, TaskSet):
-            self.user = parent.user
+            self._user = parent.user
         else:
-            self.user = parent
+            self._user = parent
 
-        self.parent = parent
+        self._parent = parent
         
         # if this class doesn't have a min_wait, max_wait or wait_function defined, copy it from Locust
         if not self.min_wait:
@@ -246,9 +240,21 @@ class TaskSet(object, metaclass=TaskSetMeta):
         if not self.wait_function:
             self.wait_function = self.user.wait_function
 
+
+
+    @property
+    def user(self):
+        """:py:class:`User <locust.User>` instance that this TaskSet was created by"""
+        return self._user
+
+    @property
+    def parent(self):
+        """Parent TaskSet instance of this TaskSet (or :py:class:`User <locust.User>` if this is not a nested TaskSet)"""
+        return self._parent
+
     def on_start(self):
         """
-        Called when a User starts executing (enters) this TaskSet
+        Called when a User starts executing this TaskSet
         """
         pass
     
@@ -392,8 +398,7 @@ class TaskSet(object, metaclass=TaskSetMeta):
     @property
     def client(self):
         """
-        Reference to the :py:attr:`client <locust.User.client>` attribute of the root
-        User instance.
+        Shortcut to the client :py:attr:`client <locust.User.client>` attribute of this TaskSet's :py:class:`User <locust.User>`
         """
         return self.user.client
 


Test patch:
diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py
index 92960c0a..dbf58d43 100644
--- a/locust/test/test_locust_class.py
+++ b/locust/test/test_locust_class.py
@@ -383,6 +383,25 @@ class TestTaskSet(LocustTestCase):
         self.assertTrue(isinstance(parents["sub"], RootTaskSet))
         self.assertTrue(isinstance(parents["subsub"], SubTaskSet))
 
+    def test_user_is_read_only(self):
+        class MyTaskSet(TaskSet):
+            raised_attribute_error = False
+            @task
+            def overwrite_user(self):
+                try:
+                    self.user = "whatever"
+                except AttributeError:
+                    MyTaskSet.raised_attribute_error = True
+                raise StopUser()
+
+        class MyUser(User):
+            wait_time = constant(0)
+            host = ""
+            tasks = [MyTaskSet]
+        
+        MyUser(Environment()).run()
+        self.assertTrue(MyTaskSet.raised_attribute_error)
+
 
 class TestLocustClass(LocustTestCase):
     def test_locust_wait(self):