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

Negating the matcher #15

Merged
merged 30 commits into from
Apr 19, 2017
Merged

Negating the matcher #15

merged 30 commits into from
Apr 19, 2017

Conversation

hieueastagile
Copy link

@hieueastagile hieueastagile commented Nov 15, 2016

Description

  • Add not_to syntax

Note

  • This PR is in-progress and for proof of concept only. Further refactor needed to make it final.
  • There're a few issues that might need to address before it could be an official version:
    • How should the failure message be changed?
    • Any other should be affected?
    • A lot of integration tests should be added
    • Double negated matcher?

Relevant issues

@hieueastagile
Copy link
Author

@tklarryonline @steadykhoi @nhanbt @hotay please help taking a look on this one.

@coveralls
Copy link

coveralls commented Nov 15, 2016

Coverage Status

Coverage increased (+0.006%) to 99.662% when pulling b59b393 on feature/expect-not into de515f3 on master.

@hieueastagile hieueastagile changed the title First draft version for negated matcher First draft version for negating the matcher Nov 15, 2016
robber/expect.py Outdated
self.to = self
self.be = self
self.a = self
self.an = self
self.have = self
Copy link

@anhhuy1605 anhhuy1605 Nov 16, 2016

Choose a reason for hiding this comment

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

Should we have self.is_negated = False for each property here? 🤔

Copy link
Author

Choose a reason for hiding this comment

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

@anhhuy1605 Not yet, I think. I will think about it again when I consider about the double (triple...) negated matcher.

Choose a reason for hiding this comment

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

I think there should be a not property that trigger is_negated as well.

Copy link
Author

Choose a reason for hiding this comment

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

We should have a function something like negate to toggle the is_negated to handle the double/triple... negating as well, it will come in the next commit.

robber/expect.py Outdated
.match()
klass(self.obj, other, self.is_negated, *args) \
.fail_with(self.message) \
.match()

Choose a reason for hiding this comment

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

What's with the outrageous indentation?

Copy link
Author

Choose a reason for hiding this comment

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

I think this indentation is correct, the previous code of robber.py didn't use PEP8 or flake8 and I fixed this when I see the warnings.

Choose a reason for hiding this comment

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

I think it's better to declare a method here, rather than making a big lambda like this...

Copy link
Author

Choose a reason for hiding this comment

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

@tklarryonline It's old code, I just keep it the same to make the proof of concept work first. Will do the refactor later.

robber/expect.py Outdated
@property
def not_to(self):
self.is_negated = True
return self

Choose a reason for hiding this comment

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

if we have a not property then this is not needed. Since you could just write .not.to.

Copy link
Author

Choose a reason for hiding this comment

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

not is Python keyword, I think that we should avoid it.

expect(1).not_to.eq(2)
expect([1, 2]).not_to.eq([2, 1])
expect((1, 2)).not_to.eq((2, 1))
expect(1).not_to == 2

Choose a reason for hiding this comment

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

The grammatically correct way to write these is:

expect(<something>).to.not.eq(<something>)

Copy link
Author

Choose a reason for hiding this comment

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

to_not and not_to are both correct, but not_to is more popular, see the discussion here: #13

Choose a reason for hiding this comment

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

Haha, I was just about to say the same thing.

self.args = args

def fail_with(self, message):
self.message = message
return self

def match(self):
if not self.matches():
if not self.matches() and not self.is_negated:
message = self.message or self.failure_message()
raise BadExpectation(message)

Choose a reason for hiding this comment

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

How about self.matches() and self.is_negated?

Copy link
Author

Choose a reason for hiding this comment

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

It should be, I made it quick to see if it works, the big refactor will be coming to address the remaining issues that I listed in the PR description.

@coveralls
Copy link

coveralls commented Nov 16, 2016

Coverage Status

Coverage increased (+0.008%) to 99.664% when pulling b331815 on feature/expect-not into de515f3 on master.

.gitignore Outdated
@@ -34,3 +34,5 @@ nosetests.xml
.mr.developer.cfg
.project
.pydevproject
.python-version
.idea/

Choose a reason for hiding this comment

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

Missing a blank line here

Copy link
Author

Choose a reason for hiding this comment

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

Great to see your comment here @catriuspham. Yes, you are right, I will add it soon.

expect(Within(1, 0, 2).matches()) == True
expect(Within(2, 3, 4).matches()) == False
expect(Within(1, 0, False, 2).matches()) == True
expect(Within(2, 3, False, 4).matches()) == False
Copy link

@catriuspham catriuspham Nov 17, 2016

Choose a reason for hiding this comment

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

I think the boolean value should be at the end or the beginning of the variable set. Like:
expect(Within(1, 0, 2, False).matches()) == True or expect(Within(False, 1, 0, 2).matches()) == True

Copy link
Author

Choose a reason for hiding this comment

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

@catriuspham You are right, but I think we can't do it here. Because this Within was extended from Base, which is actually defined like this:

class Base:
  def __init__(actual, expected, is_negated, *args):
    pass

The last value of Within is actually the agrs[0], then we can't control it.

@@ -11,7 +11,9 @@ def matches(self):
return self.actual == True

def failure_message(self):
return 'Expected %s to be True' % self.actual
negated_message = ' not' if self.is_negated else ''
return 'Expected %s%s to be True' % (self.actual, negated_message)
Copy link

@catriuspham catriuspham Nov 17, 2016

Choose a reason for hiding this comment

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

how about moving the space to between the %s, like this:

negated_message = 'not' if self.is_negated else ''
return 'Expected %s %s to be True' % (self.actual, negated_message)

Copy link
Author

@hieueastagile hieueastagile Nov 17, 2016

Choose a reason for hiding this comment

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

If we move the space into the %s, when we don't have not_to, it will be something like this:

Expected False  to be true

When we just want this:

Expected False to be true

@oyster
Copy link

oyster commented Dec 5, 2016

Please resolve the conflicts for this pull request. Thanks.

@hieueastagile
Copy link
Author

@oyster This PR is not mergeable yet, we still have a lot of things to do, I will update it in the PR description later.

@oyster
Copy link

oyster commented Dec 5, 2016

Thanks, please update and add a label like WIP to this pull request.

@hieueastagile
Copy link
Author

@oyster I had that intention, but it looks like I don't have enough permission to do so.

@oyster oyster added the WIP label Dec 5, 2016
@oyster
Copy link

oyster commented Dec 5, 2016

Ok, I created a new label WIP and added it here.

@tklarryonline
Copy link

How is it now, @hieueastagile ?

@hieueastagile
Copy link
Author

@tklarryonline Still have no time for it :(. It still had a lot of things to do with the failure_message.

@catriuspham
Copy link

I'm kind of free now, how can I contribute?

@tklarryonline
Copy link

@catriuspham Let's start with building this repo locally :)

@hieueastagile
Copy link
Author

@catriuspham Let's start with set-up this repo in your local as @tklarryonline said. After that, you can look up to the issues, there's a lot of available stuff you can do there. If you need any help, feel free to raise it at #python channel.

@catriuspham
Copy link

OK 😄

- Move negated_message into base
- Remove unused __str__ function in BadExpectation
- Use string format in failure massage
- Change "to not" to "not to"
@coveralls
Copy link

Coverage Status

Coverage increased (+0.3%) to 100.0% when pulling b7838e0 on feature/expect-not into 576f050 on master.

Now we can register a negative matcher using the positive one. The
failure message need to be customized if needed. The benefit is that
we don't have to create a whole new class for those negative matchers.

- Replace False matcher with negative True matcher
- Replace Exclude/NotContain matcher with negative Contain matcher
- Replace Falsy matcher with negative Truthy matcher
robber/expect.py Outdated
self.object = object
def __init__(self, obj):
self.obj = obj
self.is_negative = False
Copy link
Author

Choose a reason for hiding this comment

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

Hm, I guess is_negated is actually a better name, guy @catriuspham :P.

Choose a reason for hiding this comment

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

@hieueastagile Could you explain why? As I mentioned in this commit de70d8c

Base on OALD, "negative" means "containing a word such as 'no', 'not',
'never', etc.". Moreover, I cannot find the word "negated".

Copy link
Author

Choose a reason for hiding this comment

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

@catriuspham See this definition: http://www.dictionary.com/browse/negate. negated is closer to the meaning of what we are going to do. I think you can find it if you take a look around to another assertion library also.

Copy link

@catriuspham catriuspham Apr 7, 2017

Choose a reason for hiding this comment

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

@hieueastagile well, you can take a look at the fourth meaning in OALD: http://www.oxfordlearnersdictionaries.com/definition/english/negative_1?q=negative.
It literally means "containing a word such as ‘no’, ‘not’, ‘never’, etc.".
Another source is Google Trends: https://trends.google.com/trends/explore?q=negative,negated
You can see that "negative" is more widely used, while the usage of "negated" is really limited.

Copy link
Author

Choose a reason for hiding this comment

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

I don't say about the trend here, since negate is not very popular, of course. But the meaning of it is something like the reversing the matcher's meaning, which is obviously closer to what we are going to do. Because logically, you can do something like not_to.not_to, it contains not but actually it's not negative, it's just negated twice. Anyway, it's not too bad then.

robber/expect.py Outdated
cls.matchers[name] = klass

def method(self, other=None, *args):
return klass(self.object, other, *args).fail_with(self.message).match()
fact = is_negative is not self.is_negative
Copy link
Author

Choose a reason for hiding this comment

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

What do you really want to check here @catriuspham ? is_negative != self.is_negative?

Copy link

@catriuspham catriuspham Apr 6, 2017

Choose a reason for hiding this comment

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

Let's take a look at how I registered the boolean matchers.

expect.register('true', TrueMatcher)
expect.register('false', TrueMatcher, is_negative=True)

self.is_negative is the initial nature of the matcher. As for the TrueMatcher here, it it False.
is_negative is how we want to use it. As for the TrueMatcher registered as false here, it it True.
is_negative != self.is_negative decide the final nature of the matcher. I will put a fact table here:

self.is_negative is_negative Fact
True True False
True False True
False True True
False False False

From this fact table, I got that formula.

Copy link
Author

Choose a reason for hiding this comment

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

Hm, I rather write a FalseMatcher to make it cleaner than re-use the TrueMatcher in this case. It seems magical to me. Anyway, do it your choice then.

Choose a reason for hiding this comment

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

Well, I agree with you that it's (really) magical. But only the implementation is complicated, and we only have to do it once. This way of registration is really easy to use, DRY and saves time. So in my opinion, the trade-off is acceptable.

Copy link
Author

Choose a reason for hiding this comment

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

You can do it your way, but FYI, explicit/easy to read/easy to understand is always prefer than DRY, especially for python code.

@hieueastagile
Copy link
Author

@catriuspham Could you do a list what you have finished and which not? (please ignore it if it takes you a lot of time). Then I can do help if I have some free time.

@catriuspham
Copy link

@hieueastagile Actually, I have finished all the not expectation ^^ Please help review all the commits.

@coveralls
Copy link

coveralls commented Apr 6, 2017

Coverage Status

Coverage increased (+0.3%) to 100.0% when pulling 44edf78 on feature/expect-not into 54d5657 on master.

@catriuspham
Copy link

@hieueastagile As I have finished working on this PR, please help change the PR title into something like:

[f] Negative expectations

And remove the WIP tag also

@hieueastagile
Copy link
Author

Negating the matcher is the correct title, I think we don't need to change it. About the WIP, you need to ask @oyster.

Anyway, there's a case the I didn't see it here: Double negated matcher, if we plan to not allow it, I think we should raise the exception for the case: not_to.not_to or the case with more not_to.

@tklarryonline
Copy link

@hieueastagile I thought you agreed to let it be out there for fun...

But yeah, that would lead to an overflow (in some case) so I think we could raise an exception for that. ISeeWhatYouDidThereException...

@hieueastagile
Copy link
Author

lol, up to @catriuspham then @tklarryonline

@oyster oyster removed the WIP label Apr 7, 2017
@catriuspham
Copy link

@hieueastagile About the title, I mean we don't need that "First draft version" anymore, as this PR becomes really "serious" now 😂

@catriuspham
Copy link

@tklarryonline I tried to support the "fun stuff" but then realized that it was not so easy so... My apologies...

@hieueastagile hieueastagile changed the title First draft version for negating the matcher Negating the matcher Apr 7, 2017
- not_to.not_to.not_to.not_to.not_to.not_to.eq(something) is available
- rewrite the registration logic to make it easier to understand
@catriuspham
Copy link

@tklarryonline I found out an easy way to make multiple negative chains work 😛

@coveralls
Copy link

coveralls commented Apr 8, 2017

Coverage Status

Coverage increased (+0.3%) to 100.0% when pulling 9dbecb7 on feature/expect-not into 54d5657 on master.

@tklarryonline
Copy link

tklarryonline commented Apr 8, 2017

@catriuspham Great to know! 😀 Anyhow, I think @hieueastagile doesn't really like it... But yeah it's up to you hehe


expect(1).not_to.ne(2)
expect(1).not_to.not_to.not_to.ne(2)
expect(1).not_to.not_to.not_to.not_to.not_to.ne(2)

Choose a reason for hiding this comment

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

Have to admit that reading this cause my little brain blows 😂, but it's fun figuring these chains.

Choose a reason for hiding this comment

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

Legend says that if you read it out loud, you gonna become a rapper 🙄



expect.register('true', TrueMatcher)
expect.register('false', FalseMatcher)
expect.register('false', TrueMatcher, is_negative=True)

Choose a reason for hiding this comment

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

I think the name TrueMatcher is quite confusing as this class also supports False cases...

- Rename TrueMatcher to Boolean
@catriuspham
Copy link

@NhanHo I think you want to take a look :)

@coveralls
Copy link

coveralls commented Apr 10, 2017

Coverage Status

Coverage increased (+0.3%) to 100.0% when pulling 73155be on feature/expect-not into 54d5657 on master.

This was referenced Apr 10, 2017
@hieueastagile
Copy link
Author

@NhanHo please take a look on this soon. Thanks

@hieueastagile
Copy link
Author

@oyster please help merging this.

@oyster oyster merged commit a7341f6 into master Apr 19, 2017
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.

8 participants