Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A hypothesis test which loads object template from json file and create hypothesis object #44

Merged
merged 20 commits into from
Feb 3, 2020

Conversation

ke-zhang-rd
Copy link

Hypothesis object created by this process based on container.json will look like

Container(contents={Sample(composition='', description='', name='\U000e4a00\U00064a2a\t\U0008e662\n', projects=[], tags=['\U00056c15\U00086a09𗊚\U000ecaa6\x14\U000f3e75\x00\U001036f9\U000423dd\U000f195a\x06\r\x11\U0001a150\U0003f389\U0010ce9b\x15\x12\U00092be8#\U000f6d39', '\U000fb6fc\x1a\U0010c04e\x14\U000382c8']): 'LOCATION', Sample(composition='', description='', name='\x07#\U000847f8\U000f6578', projects=[], tags=[]): 'LOCATION', Sample(composition='\U00060b73/"', description='\U00046df7\x1d\U000749df(\x1f', name='/ \U000f49a4\U0008d7cf', projects=[], tags=[]): 'LOCATION', Sample(composition='', description='', name='\x17\x1c\x17\x19\x1b', projects=[], tags=['#\x1b\U000be9ef\U0009b5b6', '\x1e\U0007003a', '\U00086d1c\U00107cc6)-.\x1d\U00064fa4', '\U000a42f2\x14\U0007279e', '', '+', '', '\U00107542', '&%""\U000bcb76!\U0001ed63\U000385a0#\U00058815\x06!\U000fd3c0\x01\x1c,𐑄.\x07\U0010f0d4', '\x1a⮊', '', '\U000e7bfd\x16\U0003909e', '\U000cf4bc', '*\x08\x04\x16.\U000f1290\x11\x04ᾈ\x12\U0010d905', '\t\x1f']): 'LOCATION', Sample(composition='', description='', name='\x11\U0003eeaf\U0005c7b7\U0005a192', projects=[], tags=[]): 'LOCATION'}, kind='\U00109223𥉽', name='!! \x15-')

This also could be a beginning point for who want to try using hypothesis test/package later.

"required": [
"uuid",
"revision",
"name"
"name",
"kind",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this testing PR have to change what is required?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In code here, both kind and content needed.
https://github.com/NSLS-II/amostra/blob/master/amostra/objects.py#L175

In design,
contents needed make sense, not sure about kind

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't have really strong connection with this PR. I updated them to make fake container works(has kind and contents).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, then let's leave the schema as is. Schema changes should be motivated by user needs, not testing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why you marked this as "resolved".

amostra/schemas/sample.json Show resolved Hide resolved
amostra/schemas/sample.json Show resolved Hide resolved
amostra/tests/test_jsonschema.py Outdated Show resolved Hide resolved
amostra/tests/test_jsonschema.py Show resolved Hide resolved
container_dict = load_schema("container.json")
container_dict['properties'].pop('uuid')
container_dict['properties'].pop('revision')
container_dict['required'] = ['name', 'kind', 'contents']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as above... it would be good if the only change we had to make here was popping the read-only keys, uuid and revision.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think changing the schema before testing cuts against the spirit of how to use hypothesis. We have to dispense with uuid and required become of how our API works, but it would be better not to mutate anything else about the schema.

amostra/tests/test_jsonschema.py Outdated Show resolved Hide resolved
amostra/tests/test_jsonschema.py Outdated Show resolved Hide resolved
amostra/tests/test_jsonschema.py Outdated Show resolved Hide resolved
amostra/tests/test_jsonschema.py Outdated Show resolved Hide resolved
@ke-zhang-rd
Copy link
Author

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
~/miniconda3/envs/py3/lib/python3.7/site-packages/traitlets/traitlets.py in get(self, obj, cls)
    527         try:
--> 528             value = obj._trait_values[self.name]
    529         except KeyError:

KeyError: 'projects'

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
~/miniconda3/envs/py3/lib/python3.7/site-packages/traitlets/traitlets.py in get(self, obj, cls)
    527         try:
--> 528             value = obj._trait_values[self.name]
    529         except KeyError:

KeyError: 'projects'

During handling of the above exception, another exception occurred:

RecursionError                            Traceback (most recent call last)
~/amostra/amostra/revert_test.py in <module>
      5 db_name = str(uuid.uuid4())
      6 client = amostra.mongo_client.Client('mongodb://localhost:27017/' + db_name)
----> 7 s = client.samples.new(name = '')

~/amostra/amostra/mongo_client.py in new(self, *args, **kwargs)
    147 
    148     def new(self, *args, **kwargs):
--> 149         return self._client._new_document(self._obj_type, args, kwargs)
    150 
    151     def find(self, filter):

~/amostra/amostra/mongo_client.py in _new_document(self, obj_type, args, kwargs)
     59 
     60         # Insert the new object.
---> 61         collection.insert_one(obj.to_dict())
     62 
     63         # Observe any updates to the object and sync them to MongoDB.

~/amostra/amostra/objects.py in to_dict(self)
     57         Represent the object as a JSON-serializable dictionary.
     58         """
---> 59         return {name: getattr(self, name) for name in self.trait_names()}
     60 
     61     @classmethod

~/amostra/amostra/objects.py in <dictcomp>(.0)
     57         Represent the object as a JSON-serializable dictionary.
     58         """
---> 59         return {name: getattr(self, name) for name in self.trait_names()}
     60 
     61     @classmethod

~/miniconda3/envs/py3/lib/python3.7/site-packages/traitlets/traitlets.py in __get__(self, obj, cls)
    554             return self
    555         else:
--> 556             return self.get(obj, cls)
    557 
    558     def set(self, obj, value):

~/miniconda3/envs/py3/lib/python3.7/site-packages/traitlets/traitlets.py in get(self, obj, cls)
    533                 raise TraitError("No default value found for %s trait of %r"
    534                                  % (self.name, obj))
--> 535             value = self._validate(obj, dynamic_default())
    536             obj._trait_values[self.name] = value
    537             return value

~/miniconda3/envs/py3/lib/python3.7/site-packages/traitlets/traitlets.py in _validate(self, obj, value)
    591             value = self.validate(obj, value)
    592         if obj._cross_validation_lock is False:
--> 593             value = self._cross_validate(obj, value)
    594         return value
    595 

~/miniconda3/envs/py3/lib/python3.7/site-packages/traitlets/traitlets.py in _cross_validate(self, obj, value)
    597         if self.name in obj._trait_validators:
    598             proposal = Bunch({'trait': self, 'value': value, 'owner': obj})
--> 599             value = obj._trait_validators[self.name](obj, proposal)
    600         elif hasattr(obj, '_%s_validate' % self.name):
    601             meth_name = '_%s_validate' % self.name

~/miniconda3/envs/py3/lib/python3.7/site-packages/traitlets/traitlets.py in __call__(self, *args, **kwargs)
    905         """Pass `*args` and `**kwargs` to the handler's function if it exists."""
    906         if hasattr(self, 'func'):
--> 907             return self.func(*args, **kwargs)
    908         else:
    909             return self._init_call(*args, **kwargs)

~/amostra/amostra/objects.py in _validate_with_jsonschema(instance, proposal)
     22     This is meant to be used with traitlets' @validate decorator.
     23     """
---> 24     jsonschema.validate(instance.to_dict(), instance.SCHEMA)
     25     return proposal['value']
     26 

... last 8 frames repeated, from the frame below ...

~/amostra/amostra/objects.py in to_dict(self)
     57         Represent the object as a JSON-serializable dictionary.
     58         """
---> 59         return {name: getattr(self, name) for name in self.trait_names()}
     60 
     61     @classmethod

RecursionError: maximum recursion depth exceeded

The error was observed when try to init a Sample with only empty string name

from pymongo import MongoClient
import uuid
import amostra.mongo_client

db_name = str(uuid.uuid4())
client = amostra.mongo_client.Client('mongodb://localhost:27017/' + db_name)
s = client.samples.new(name='') 

The reason isn't clear yet. I suspect it come from traitlets's Unicode.
Right now, in sample.json, set "minLength": 1 to go around this error.

@danielballan
Copy link
Contributor

Interesting. I think it would be good to understand the cause before merging. Can you start with something minimal like:

class Thing(amostra.objects.AmostraDocument): 
    stuff = traitlets.List(traitlets.Unicode()) 

@ke-zhang-rd
Copy link
Author

ke-zhang-rd commented Sep 3, 2019

class Thing(amostra.objects.AmostraDocument): 

I'll try. Do you have clue/direction why name(which is Unicode()) empty or not could influence projects which is List(Unicode()) behavior?

@ke-zhang-rd
Copy link
Author

ke-zhang-rd commented Sep 5, 2019

After some search online, I feel typical way to use __new__ is

def __new__(cls, *args, **kwargs):
    inst = super().__new__(cls)
    '''
    manipulate inst
    '''
    return inst

Also in traitlets examples here, looks validate method bond with instance instead of class.

class Parity(HasTraits):
    value = Int()
    parity = Int()

    @validate('value')
    def _valid_value(self, proposal):

cls._validate = validate(*trait_names)(_validate_with_jsonschema)
return super().__new__(cls, *args, **kwargs)
instance = super().__new__(cls, *args, **kwargs)
instance._validate = validate(*trait_names)(_validate_with_jsonschema)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think think that this change makes the validation not happen at al! The validate method produces a descriptor which needs to be in place for the super().__new__(...) to find them and do something about it.

Are there any tests we we are sure that we do reject invalid documents?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think @tacaswell is right. It would be good to add a test that uses pytest.raises to ensure that validation is running and correctly raising an error on invalid inputs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you that validation wasn't triggered.

@tacaswell
Copy link
Member

Inlt [24]: s                                                                                                                                                                                                                                                    
Out[24]: amostra.objects.Sample

In [25]: s(None, name='') 

is enough to trigger the recursion. I suspect this is due to '' being the default value of name and it looks like there is a loop triggered via the interplay interplay between the collective validation during setting the values and during getting the default values.

Given that we have already made 'name' special via the signature, I think we should enforce that it is not the empty sttring in the Sample init.

This was referenced Jan 29, 2020
@ke-zhang-rd
Copy link
Author

I suspect this need to be fixed?

short words about why recursion
to_dict -> _validate_with_jsonschema -> to_dict ...

@ke-zhang-rd
Copy link
Author

Maybe I should do thing below instead of every place using to_dict.

    def to_dict(self):
        """
        Represent the object as a JSON-serializable dictionary.
        """
        with self. cross_validation_lock:
            result = {name: getattr(self, name) for name in self.trait_names()}
        return result

@danielballan
Copy link
Contributor

Great, using the lock inside to_dict (and removing it from __repr__ and elsewhere) feels right. I will review again with fresh eyes next week. This is too detailed for a Friday 5pm review. :-D

Copy link
Contributor

@danielballan danielballan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-opened an old, unresolved comment and left one optional implementation suggestion.

amostra/tests/test_jsonschema.py Outdated Show resolved Hide resolved
Copy link
Contributor

@danielballan danielballan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Thanks for your persistence.

@danielballan danielballan merged commit 7c6bbb3 into NSLS-II:master Feb 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants